From 31d756672c1888973be53dfc84444cb4260158c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yannik=20R=C3=B6del?= Date: Tue, 18 Nov 2025 19:07:47 +0100 Subject: [PATCH] Migrate gaps to CSS variables --- styles/base.scss | 4 +- styles/components/_actions.scss | 26 ++++++------- styles/components/_banner.scss | 10 ++--- styles/components/_form-choices.scss | 16 ++++---- styles/components/_form-elements.scss | 16 ++++---- styles/components/_markup.scss | 52 ++++++++++++------------- styles/components/_page.scss | 8 ++-- styles/components/_site.scss | 14 ++++--- styles/components/_tabs.scss | 4 +- styles/components/_timeline.scss | 22 +++++------ styles/finish.scss | 2 +- styles/lib/_fluids.scss | 56 +++++++++++++++++++++++++++ styles/lib/_layout.scss | 29 +++++++++----- styles/lib/_typography.scss | 2 +- 14 files changed, 167 insertions(+), 94 deletions(-) create mode 100644 styles/lib/_fluids.scss diff --git a/styles/base.scss b/styles/base.scss index c6a38b4..6c8ce38 100644 --- a/styles/base.scss +++ b/styles/base.scss @@ -1,11 +1,13 @@ @use 'lib/colors'; +@use 'lib/layout'; @use 'lib/typography'; @use 'components/markup'; @use 'components/site'; @use 'components/page'; @use 'components/banner'; -@include typography.root-config; +@include layout.setup; +@include typography.setup; * { box-sizing: border-box; diff --git a/styles/components/_actions.scss b/styles/components/_actions.scss index e0d47c3..89dda24 100644 --- a/styles/components/_actions.scss +++ b/styles/components/_actions.scss @@ -5,8 +5,8 @@ // Actions are another module that is present on the home page. It shows a small // number of CTA-style buttons which lead to different parts of the site. .page-actions { - padding-top: layout.$large-gap; - padding-bottom: layout.$large-gap; + padding-top: var(--gap-l); + padding-bottom: var(--gap-l); background-color: colors.$teal-300; @include colors.coderdojo-theme { @@ -14,7 +14,7 @@ } > *:not(:last-child) { - margin-bottom: layout.$large-gap; + margin-bottom: var(--gap-l); } > div { @@ -31,8 +31,8 @@ display: flex; flex-direction: column-reverse; align-items: center; - margin: 0 layout.$normal-gap; - padding: layout.$normal-gap; + margin: 0 var(--gap-m); + padding: var(--gap-m); background: colors.$gray-50; color: inherit; text-decoration: none; @@ -48,12 +48,12 @@ } > div > .inline-callout { - margin-top: layout.$normal-gap; + margin-top: var(--gap-m); } > svg { align-self: center; - margin-bottom: layout.$normal-gap; + margin-bottom: var(--gap-m); height: 10rem; } @@ -63,8 +63,8 @@ > span, > em { - margin-top: layout.$small-gap; - margin-bottom: layout.$small-gap; + margin-top: var(--gap-s); + margin-bottom: var(--gap-s); } &:hover { @@ -80,10 +80,10 @@ @media screen and (min-width: layout.$breakpoint) { display: grid; grid-template-columns: repeat(6, 1fr); - gap: layout.$large-gap; + gap: var(--gap-l); align-items: start; justify-items: stretch; - padding: layout.$large-gap; + padding: var(--gap-l); > * { grid-column: span 3; @@ -95,13 +95,13 @@ > a { margin: 0; - padding: layout.$large-gap; + padding: var(--gap-l); &:first-of-type { flex-direction: row; justify-content: space-between; position: relative; - margin-top: #{-1 * layout.$huge-gap}; + margin-top: calc(-1 * var(--gap-2xl)); } &:not(:first-of-type) { diff --git a/styles/components/_banner.scss b/styles/components/_banner.scss index fa583ca..134f7a7 100644 --- a/styles/components/_banner.scss +++ b/styles/components/_banner.scss @@ -41,7 +41,7 @@ > div { display: inline-block; - padding: layout.$small-gap layout.$normal-gap; + padding: var(--gap-s) var(--gap-m); font-size: typography.$subheading-size; background-color: colors.$yellow-600; @@ -65,7 +65,7 @@ } > .title { - padding: 0 layout.$normal-gap; + padding: 0 var(--gap-m); line-height: 5rem; font-size: typography.$title-size; font-weight: typography.$emphasized-weight; @@ -77,7 +77,7 @@ } @media screen and (min-width: layout.$breakpoint) { - padding: layout.$normal-gap max(#{layout.$large-gap}, 15vw); + padding: var(--gap-m) max(var(--gap-l), 15vw); } } } @@ -89,12 +89,12 @@ .page-callout { @extend %callout; - padding: layout.$normal-gap; + padding: var(--gap-m); } .inline-callout { @extend %callout; display: inline-block; font-style: normal; - padding: layout.$small-gap layout.$normal-gap; + padding: var(--gap-s) var(--gap-m); } diff --git a/styles/components/_form-choices.scss b/styles/components/_form-choices.scss index f1c14fd..07d74ca 100644 --- a/styles/components/_form-choices.scss +++ b/styles/components/_form-choices.scss @@ -6,8 +6,8 @@ .form-choices { @extend %narrow-content; - margin-top: layout.$huge-gap; - margin-bottom: layout.$huge-gap; + margin-top: var(--gap-2xl); + margin-bottom: var(--gap-2xl); list-style: none; text-align: center; @@ -25,13 +25,13 @@ &:not(:last-child) { &:before { - top: calc(50% - #{math.div(layout.$large-gap, 2)}); + top: calc(50% - var(--gap-l) / 2); } &::after { content: ''; display: block; - margin: layout.$large-gap auto; + margin: var(--gap-l) auto; width: 15rem; height: 1px; background-color: colors.$gray-300; @@ -44,8 +44,8 @@ } &.narrow { - margin-top: layout.$large-gap; - margin-bottom: layout.$large-gap; + margin-top: var(--gap-l); + margin-bottom: var(--gap-l); > li::after { display: none; @@ -54,7 +54,7 @@ } %form-choice-link { - padding: layout.$small-gap layout.$normal-gap; + padding: var(--gap-s) var(--gap-m); display: inline-block; line-height: 2.5; text-decoration: none; @@ -92,7 +92,7 @@ } > em { - padding-block: layout.$small-gap; + padding-block: var(--gap-s); font-style: inherit; background-color: colors.$yellow-600; transition: motion.$subtle background-color; diff --git a/styles/components/_form-elements.scss b/styles/components/_form-elements.scss index 8025e33..e3e5684 100644 --- a/styles/components/_form-elements.scss +++ b/styles/components/_form-elements.scss @@ -44,7 +44,7 @@ %form-input { @extend %base-form-input; - padding: layout.$small-gap; + padding: var(--gap-s); font: inherit; transition: motion.$subtle background-color; @@ -81,11 +81,11 @@ > :last-child { @extend %form-input; flex-basis: 60%; - margin-top: layout.$small-gap; + margin-top: var(--gap-s); @media screen and (min-width: layout.$breakpoint) { margin-top: 0; - margin-left: layout.$normal-gap; + margin-left: var(--gap-m); } } } @@ -102,7 +102,7 @@ content: '\2771'; display: inline-block; position: absolute; - left: #{layout.$large-gap * -1}; + left: calc(-1 * var(--gap-l)); top: 50%; transform: translateY(-50%); opacity: 0.3; @@ -140,7 +140,7 @@ .form-checkbox { $size: 2rem; - $gap: layout.$small-gap; + $gap: var(--gap-s); @extend %form-item; justify-content: flex-end; @@ -193,7 +193,7 @@ .blanked-out-form { position: relative; - padding: layout.$large-gap 0; + padding: var(--gap-l) 0; pointer-events: none; &::after { @@ -220,8 +220,8 @@ ); mask-image: linear-gradient( transparent 0%, - black #{layout.$large-gap}, - black calc(100% - #{layout.$large-gap}), + black var(--gap-l), + black calc(100% - var(--gap-l)), transparent 100% ); } diff --git a/styles/components/_markup.scss b/styles/components/_markup.scss index 15825ea..805321c 100644 --- a/styles/components/_markup.scss +++ b/styles/components/_markup.scss @@ -10,8 +10,8 @@ body { %title { @extend %content-gutter; - margin-top: layout.$huge-gap; - margin-bottom: layout.$large-gap; + margin-top: var(--gap-2xl); + margin-bottom: var(--gap-l); font-size: typography.$title-size; line-height: typography.$heading-line-height; text-align: center; @@ -24,8 +24,8 @@ h1 { %heading { @extend %content-gutter; - margin-top: layout.$large-gap; - margin-bottom: layout.$large-gap; + margin-top: var(--gap-l); + margin-bottom: var(--gap-l); font-size: typography.$heading-size; line-height: typography.$heading-line-height; overflow-wrap: anywhere; @@ -72,11 +72,11 @@ dl { } li { - margin: layout.$small-gap 0; + margin: var(--gap-s) 0; } dt { - margin: layout.$normal-gap 0; + margin: var(--gap-m) 0; font-weight: typography.$emphasized-weight; color: colors.$blue-800; @@ -86,12 +86,12 @@ dt { } dd { - margin: layout.$normal-gap 0 layout.$normal-gap layout.$large-gap; + margin: var(--gap-m) 0 var(--gap-m) var(--gap-l); } em { background-color: colors.$teal-300; - padding: 0 layout.$small-gap 0 layout.$small-gap; + padding: 0 var(--gap-s) 0 var(--gap-s); @include colors.coderdojo-theme { background-color: colors.$orange-300; @@ -120,8 +120,8 @@ blockquote { } @extend %narrow-content-gutter; - padding-left: layout.$small-gap; - padding-right: layout.$small-gap; + padding-left: var(--gap-s); + padding-right: var(--gap-s); $border: 0.5em solid $color; border-top: $border; border-bottom: $border; @@ -137,7 +137,7 @@ blockquote { .cta-link { display: block; margin: 0 auto; - padding: 0 layout.$normal-gap; + padding: 0 var(--gap-m); max-width: layout.$narrow-content-width; text-align: center; text-decoration: none; @@ -162,7 +162,7 @@ blockquote { ul.link-grid { display: grid; grid-template-columns: 1fr; - gap: layout.$larger-gap layout.$normal-gap; + gap: var(--gap-xl) var(--gap-m); justify-items: center; align-items: center; padding: 0; @@ -174,7 +174,7 @@ ul.link-grid { } &.large { - gap: layout.$normal-gap; + gap: var(--gap-m); > li > a { display: flex; @@ -182,7 +182,7 @@ ul.link-grid { > img { max-width: 6rem; - margin-right: layout.$normal-gap; + margin-right: var(--gap-m); } } } @@ -195,7 +195,7 @@ ul.link-grid { > li { > a { display: inline-block; - padding: layout.$normal-gap; + padding: var(--gap-m); text-align: center; text-decoration: none; color: colors.$gray-600; @@ -205,7 +205,7 @@ ul.link-grid { > img { display: block; - margin: 0 auto layout.$small-gap auto; + margin: 0 auto var(--gap-s) auto; max-width: 100%; max-height: 6rem; } @@ -222,21 +222,21 @@ ul.link-grid { flex-direction: column; align-items: stretch; @extend %content-gutter; - margin-top: layout.$huge-gap; - margin-bottom: layout.$huge-gap; + margin-top: var(--gap-2xl); + margin-bottom: var(--gap-2xl); @media screen and (min-width: layout.$breakpoint) { display: grid; grid-template-columns: 1fr 1fr; - gap: layout.$normal-gap; + gap: var(--gap-m); justify-content: stretch; } } :any-link.post-card { display: block; - padding-top: layout.$normal-gap; - padding-bottom: layout.$normal-gap; + padding-top: var(--gap-m); + padding-bottom: var(--gap-m); text-decoration: none; transition: background-color motion.$subtle, @@ -244,8 +244,8 @@ ul.link-grid { box-shadow motion.$subtle; @media screen and (min-width: layout.$breakpoint) { - padding-left: layout.$normal-gap; - padding-right: layout.$normal-gap; + padding-left: var(--gap-m); + padding-right: var(--gap-m); } &:hover { @@ -298,7 +298,7 @@ ul.link-grid { } + time { - margin-top: -1 * layout.$normal-gap; + margin-top: calc(-1 * var(--gap-m)); } } @@ -307,7 +307,7 @@ ul.link-grid { color: colors.$gray-600; + p { - margin-top: layout.$small-gap; + margin-top: var(--gap-s); } } } @@ -322,7 +322,7 @@ ul.link-grid { > img { display: block; - margin: 0 auto layout.$small-gap auto; + margin: 0 auto var(--gap-s) auto; max-height: 6rem; } } diff --git a/styles/components/_page.scss b/styles/components/_page.scss index e7a4c8f..466d63d 100644 --- a/styles/components/_page.scss +++ b/styles/components/_page.scss @@ -3,11 +3,11 @@ @use '../lib/typography'; .page-section { - padding-top: layout.$large-gap; - padding-bottom: layout.$large-gap; + padding-top: var(--gap-l); + padding-bottom: var(--gap-l); @media screen and (min-width: layout.$breakpoint) { - padding: layout.$large-gap; + padding: var(--gap-l); } &.inverse { @@ -52,5 +52,5 @@ } main:not(.expand) + .page-section.footer { - margin-top: layout.$huge-gap; + margin-top: var(--gap-2xl); } diff --git a/styles/components/_site.scss b/styles/components/_site.scss index f91e0db..94a1070 100644 --- a/styles/components/_site.scss +++ b/styles/components/_site.scss @@ -4,14 +4,14 @@ @use '../lib/typography'; @mixin header-item { - padding: 0 layout.$large-gap; + padding: 0 var(--gap-m); line-height: 4rem; } // The site logo text. More specific styles for this element are also present // underneath .site-header. .site-logo { - margin: 0 layout.$large-gap; + margin: 0 var(--gap-l); text-transform: lowercase; white-space: nowrap; @@ -96,7 +96,7 @@ a { display: block; - padding: layout.$small-gap; + padding: var(--gap-s); font-weight: typography.$emphasized-weight; text-align: center; text-decoration: none; @@ -107,6 +107,10 @@ .site-mobile-navigation { cursor: pointer; + li { + margin-block: 0; + } + > summary { display: block; @include header-item; @@ -132,7 +136,7 @@ content: ''; display: block; margin: 0 auto; - width: calc(100% - #{2 * layout.$normal-gap}); + width: calc(100% - 2 * var(--gap-m)); height: 1px; background-color: colors.$gray-300; } @@ -174,7 +178,7 @@ display: flex; flex-wrap: wrap; justify-content: space-between; - gap: layout.$normal-gap; + gap: var(--gap-m); .site-navigation a { text-align: right; diff --git a/styles/components/_tabs.scss b/styles/components/_tabs.scss index bcfe19b..39df867 100644 --- a/styles/components/_tabs.scss +++ b/styles/components/_tabs.scss @@ -29,7 +29,7 @@ > a { display: block; - margin: 0 layout.$normal-gap; + margin: 0 var(--gap-m); text-decoration: none; color: inherit; } @@ -64,7 +64,7 @@ @media screen and (min-width: layout.$breakpoint) { > .tab { - padding: layout.$large-gap; + padding: var(--gap-l); @include colors.card-shadow; } } diff --git a/styles/components/_timeline.scss b/styles/components/_timeline.scss index d15d67e..cbd2cfa 100644 --- a/styles/components/_timeline.scss +++ b/styles/components/_timeline.scss @@ -7,7 +7,7 @@ .timeline { $stampSize: 4rem; $lineWeight: 0.2rem; - $itemSpacing: layout.$normal-gap; + $itemSpacing: var(--gap-m); @extend %content-gutter; display: flex; @@ -46,7 +46,7 @@ > time { display: block; - padding-inline: layout.$small-gap; + padding-inline: var(--gap-s); font-size: typography.$base-size; font-weight: typography.$normal-weight; } @@ -60,15 +60,15 @@ > .content { position: relative; flex-grow: 1; - margin-left: layout.$normal-gap; + margin-left: var(--gap-m); margin-bottom: $itemSpacing; &::before { content: ''; position: absolute; top: 0; - right: calc(100% + #{layout.$normal-gap + math.div($stampSize, 2)}); - bottom: #{-1 * $itemSpacing}; + right: calc(100% + var(--gap-m) + #{math.div($stampSize, 2)}); + bottom: calc(-1 * #{$itemSpacing}); width: $lineWeight; background-color: colors.$gray-300; transform: translateX(50%); @@ -78,14 +78,14 @@ > h3:first-child { position: relative; margin-top: 0; - margin-left: layout.$large-gap; + margin-left: var(--gap-l); line-height: $stampSize; text-align: left; &::after { content: ''; position: absolute; - right: calc(100% + #{layout.$small-gap}); + right: calc(100% + var(--gap-s)); top: 50%; width: 4rem; height: $lineWeight; @@ -136,12 +136,12 @@ margin-bottom: 0; > .content { - padding-bottom: layout.$normal-gap; + padding-bottom: var(--gap-m); } } + .timeline { - margin-top: #{-1 * layout.$normal-gap}; + margin-top: calc(-1 * var(--gap-m)); } } @@ -150,8 +150,8 @@ display: grid; grid-template-columns: var(--x1, 50%) minmax(0, 1fr); grid-template-rows: var(--y1, 50%) minmax(0, 1fr); - gap: layout.$tiny-gap; - padding: layout.$tiny-gap; + gap: var(--gap-xs); + padding: var(--gap-xs); height: 27rem; background-color: colors.$blue-800; @include colors.card-shadow; diff --git a/styles/finish.scss b/styles/finish.scss index 4365b94..376866e 100644 --- a/styles/finish.scss +++ b/styles/finish.scss @@ -47,7 +47,7 @@ display: block; height: 15vmin; - margin: layout.$huge-gap auto; + margin: var(--gap-2xl) auto; overflow: visible; stroke-linecap: round; stroke-miterlimit: 10; diff --git a/styles/lib/_fluids.scss b/styles/lib/_fluids.scss new file mode 100644 index 0000000..ef03085 --- /dev/null +++ b/styles/lib/_fluids.scss @@ -0,0 +1,56 @@ +@use "sass:math"; + +$min-fluid-size: 25rem !default; // 400px at 16px rem size +$max-fluid-size: 60rem !default; // 960px at 16px rem size +$fluid-base: 1vw !default; + +/// Return a CSS clamp() call that interpolates between the two given values +/// when the viewport width changes. This can be used for fluid type and spacing +/// scales. +/// +/// When the current viewport has a width less than or equal to the +/// (configurable) $min-fluid-size variable, the expression returned by this +/// function will evaluate to $from. Similarly, sizes equal to or greater than +/// $max-fluid-size will yield $to. Eveything in between will be calculated +/// using linear interpolation. +/// +/// Set the $fluid-base variable to `1vh` to use the viewport height instead of +/// its width. +/// +/// $from and $to must have the same unit or both be unitless. +@function fluid-value($from, $to) { + // Solve the following system of equations: + // a * 0.01 * $min-fluid-size + b = $from + // a * 0.01 * $max-fluid-size + b = $to + // The constant 0.01 is for converting the vw unit to an actual percentage + // factor. + $fluid-factor: math.div( + $from - $to, + 0.01 * ($min-fluid-size - $max-fluid-size) + ); // a + $static-value: $from - $fluid-factor * 0.01 * $min-fluid-size; // b + @return clamp(#{$from}, #{$fluid-factor}vw + #{$static-value}, #{$to}); +} + +$lower-harmonic-ratio: 1.125 !default; +$upper-harmonic-ratio: 1.25 !default; + +/// Calculate the value on a modular harmonic scale at a specific index +/// (exponent). The result will be a fluid CSS calc() expression. +/// +/// Use this to quickly create an entire scale of harmonic values. Using an +/// $exponent of 0 will return a fluid value that evalutes to $lower-base on +/// small viewports and $upper-base on large viewports. Positive exponents make +/// the fluid value larger, negative values decrease it. The module-level +/// configuration variables $lower-harmonic-ratio and $upper-harmonic-ratio +/// define curvature of that function (over the $exponent). +/// +/// Other notes from fluid-value() apply here as well. In particular, +/// $lower-base and $upper-base must have the same unit. $exponent must be +/// unitless. +@function harmonic-value($exponent, $lower-base, $upper-base) { + @return fluid-value( + $lower-base * math.pow($lower-harmonic-ratio, $exponent), + $upper-base * math.pow($upper-harmonic-ratio, $exponent) + ); +} diff --git a/styles/lib/_layout.scss b/styles/lib/_layout.scss index 1d5e1f6..8f7385c 100644 --- a/styles/lib/_layout.scss +++ b/styles/lib/_layout.scss @@ -1,9 +1,7 @@ -$tiny-gap: 0.3rem; -$small-gap: 0.5rem; -$normal-gap: 1.5rem; -$large-gap: 2.5rem; -$larger-gap: 4rem; -$huge-gap: 6rem; +@use "fluids" with ( + $lower-harmonic-ratio: 1.25, + $upper-harmonic-ratio: 1.3 +); $breakpoint: 80rem; @@ -18,7 +16,7 @@ $wide-content-width: 80rem; } %narrow-content-gutter { - margin: $normal-gap $normal-gap; + margin: var(--gap-m); @media screen and (min-width: $breakpoint) { margin-left: auto; @@ -34,7 +32,7 @@ $wide-content-width: 80rem; } %content-gutter { - margin: $normal-gap $normal-gap; + margin: var(--gap-m); @media screen and (min-width: $breakpoint) { margin-left: auto; @@ -50,6 +48,19 @@ $wide-content-width: 80rem; } %wide-content-gutter { - margin: $normal-gap auto; + margin: var(--gap-m) auto; max-width: $wide-content-width; } + +@mixin setup { + $-gap-lower-base: 1.5rem; + $-gap-upper-base: 2rem; + :root { + --gap-xs: #{fluids.harmonic-value(-3, $-gap-lower-base, $-gap-upper-base)}; + --gap-s: #{fluids.harmonic-value(-2, $-gap-lower-base, $-gap-upper-base)}; + --gap-m: #{fluids.harmonic-value(0, $-gap-lower-base, $-gap-upper-base)}; + --gap-l: #{fluids.harmonic-value(2, $-gap-lower-base, $-gap-upper-base)}; + --gap-xl: #{fluids.harmonic-value(3, $-gap-lower-base, $-gap-upper-base)}; + --gap-2xl: #{fluids.harmonic-value(5, $-gap-lower-base, $-gap-upper-base)}; + } +} diff --git a/styles/lib/_typography.scss b/styles/lib/_typography.scss index 6e82256..b153d5f 100644 --- a/styles/lib/_typography.scss +++ b/styles/lib/_typography.scss @@ -18,7 +18,7 @@ $comfortaa-weights: ( 'Bold': 700, ); -@mixin root-config { +@mixin setup { @each $name, $weight in $comfortaa-weights { @font-face { font-family: 'Comfortaa';