diff --git a/README.md b/README.md index 97d3c9060a..d6988b80fc 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,6 @@ In this task, you will learn how to implement a landing page. To do that: - [Nothing](https://www.figma.com/file/DtkQmQ797hk0nI4KfMi2Uq/BOSE-New-Version?type=design&node-id=6802-139&t=L7eKz5YKLN0m5WxR-0) - watch the lesson videos and implement your page blocks similarly to the videos; - **DON'T** try to do it `Pixel Perfect` - implement it the most `simple` way so it looks similar; -- when you finish the first block of your page deploy it and create a Pull Request with a [DEMO LINK](https://.github.io/layout_miami/) +- when you finish the first block of your page deploy it and create a Pull Request with a [DEMO LINK](https://inklynx.github.io/layout_miami/) - after each next block do the same (add, commit and push the changes, and deploy the updated demo; - check yourself using the [CHECKLIST](https://github.com/mate-academy/layout_miami/blob/master/checklist.md) when finished; diff --git a/index.html b/index.html index 06bcd7773a..7f15000533 100644 --- a/index.html +++ b/index.html @@ -6,17 +6,678 @@ name="viewport" content="width=device-width, initial-scale=1.0" /> - Miami + BOSE Landing + + + + + + + + + + + -

Miami

+
+ +
+ + + +
+

+ Bose Quiet Comfort Wireless Headphones - Premium Noise Cancelling Audio + Experience +

+
+ + + + woman wearing Bose headphones + +
+
+ + +

+ The world shades. +
+ Your music shines. +

+
+
+
+ + + +
+

+ Browse Bose products +
+ by category +

+ +
+
+
+ Woman wearing Bose earbuds +
+
+ Woman wearing Bose QuietComfort headphones +
+
+

Headphones & earbuds

+
+ +
+
+
+ Portable Bose speaker +
+
+ Bose Color Soundlink Portable speaker +
+
+

Speakers

+
+ +
+
+
+ Man wearing Bose Frames audio sunglasses +
+
+ Girl wearing Bose Frames collection +
+
+

Audio sunglasses

+
+ + + View all + +
+ +
+

+ Why buy direct +
+ from Bose +

+ +
+
    +
  • Free 2-day shipping and returns
  • +
  • 90-day risk-free trial
  • +
  • World class customer service
  • +
  • My Bose account management
  • +
+

+ A great product is more than what's in the box. It's a promise of + premium performance, world-class support, and everything you expect + from a trusted brand. It's just one of many reasons why you'll shop + with confidence on Bose.com. +

+
+
+ +
+

Contact us

+
+
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+ +
+ +
+
diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000000..c2a49f4fb8 --- /dev/null +++ b/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Allow: / diff --git a/src/fonts/roboto-blackitalic-webfont.woff2 b/src/fonts/roboto-blackitalic-webfont.woff2 new file mode 100644 index 0000000000..d438282504 Binary files /dev/null and b/src/fonts/roboto-blackitalic-webfont.woff2 differ diff --git a/src/fonts/roboto-bold-webfont.woff2 b/src/fonts/roboto-bold-webfont.woff2 new file mode 100644 index 0000000000..49cf2c70d8 Binary files /dev/null and b/src/fonts/roboto-bold-webfont.woff2 differ diff --git a/src/fonts/roboto-bolditalic-webfont.woff2 b/src/fonts/roboto-bolditalic-webfont.woff2 new file mode 100644 index 0000000000..2bdfedffe5 Binary files /dev/null and b/src/fonts/roboto-bolditalic-webfont.woff2 differ diff --git a/src/fonts/roboto-italic-webfont.woff2 b/src/fonts/roboto-italic-webfont.woff2 new file mode 100644 index 0000000000..df4f05f342 Binary files /dev/null and b/src/fonts/roboto-italic-webfont.woff2 differ diff --git a/src/fonts/roboto-medium-webfont.woff2 b/src/fonts/roboto-medium-webfont.woff2 new file mode 100644 index 0000000000..1f414112d8 Binary files /dev/null and b/src/fonts/roboto-medium-webfont.woff2 differ diff --git a/src/fonts/roboto-mediumitalic-webfont.woff2 b/src/fonts/roboto-mediumitalic-webfont.woff2 new file mode 100644 index 0000000000..aa2e67d665 Binary files /dev/null and b/src/fonts/roboto-mediumitalic-webfont.woff2 differ diff --git a/src/fonts/roboto-regular-webfont.woff2 b/src/fonts/roboto-regular-webfont.woff2 new file mode 100644 index 0000000000..0685e42226 Binary files /dev/null and b/src/fonts/roboto-regular-webfont.woff2 differ diff --git a/src/images/bose-fav.svg b/src/images/bose-fav.svg new file mode 100644 index 0000000000..d09c39b663 --- /dev/null +++ b/src/images/bose-fav.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/images/cross.svg b/src/images/cross.svg deleted file mode 100644 index 08f1f27cea..0000000000 --- a/src/images/cross.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/images/crown.svg b/src/images/crown.svg deleted file mode 100644 index 962d90e9ca..0000000000 --- a/src/images/crown.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/images/favicon.png b/src/images/favicon.png deleted file mode 100644 index 0e04c361e1..0000000000 Binary files a/src/images/favicon.png and /dev/null differ diff --git a/src/images/header-phone.svg b/src/images/header-phone.svg new file mode 100644 index 0000000000..3414c6680c --- /dev/null +++ b/src/images/header-phone.svg @@ -0,0 +1 @@ +Obszar roboczy 1 \ No newline at end of file diff --git a/src/images/hero-icon-sound-waves.svg b/src/images/hero-icon-sound-waves.svg new file mode 100644 index 0000000000..cb64c6b3d1 --- /dev/null +++ b/src/images/hero-icon-sound-waves.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/images/hero-logo-bose.svg b/src/images/hero-logo-bose.svg new file mode 100644 index 0000000000..117072e196 --- /dev/null +++ b/src/images/hero-logo-bose.svg @@ -0,0 +1 @@ +bose-logobose-logo \ No newline at end of file diff --git a/src/images/logo.png b/src/images/logo.png deleted file mode 100644 index 9b6a4dd7d4..0000000000 Binary files a/src/images/logo.png and /dev/null differ diff --git a/src/images/menu-burger-hover.svg b/src/images/menu-burger-hover.svg new file mode 100644 index 0000000000..33dbee4f65 --- /dev/null +++ b/src/images/menu-burger-hover.svg @@ -0,0 +1 @@ +menu-burger-hoverObszar roboczy 1 \ No newline at end of file diff --git a/src/images/menu-burger.svg b/src/images/menu-burger.svg new file mode 100644 index 0000000000..1f7f2deb28 --- /dev/null +++ b/src/images/menu-burger.svg @@ -0,0 +1 @@ +menu-burgerObszar roboczy 1 \ No newline at end of file diff --git a/src/images/menu-x.svg b/src/images/menu-x.svg new file mode 100644 index 0000000000..73655f44b1 --- /dev/null +++ b/src/images/menu-x.svg @@ -0,0 +1 @@ +menu-xObszar roboczy 1 diff --git a/src/images/menu.svg b/src/images/menu.svg deleted file mode 100644 index 5d6b5a0b38..0000000000 --- a/src/images/menu.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/images/menu_hover.svg b/src/images/menu_hover.svg deleted file mode 100644 index 39263b75bc..0000000000 --- a/src/images/menu_hover.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/images/phone.svg b/src/images/phone.svg deleted file mode 100644 index 0c2c52cb54..0000000000 --- a/src/images/phone.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/images/photos/1.jpg b/src/images/photos/1.jpg deleted file mode 100644 index f796f55605..0000000000 Binary files a/src/images/photos/1.jpg and /dev/null differ diff --git a/src/images/photos/2.jpg b/src/images/photos/2.jpg deleted file mode 100644 index d9053b6a20..0000000000 Binary files a/src/images/photos/2.jpg and /dev/null differ diff --git a/src/images/photos/3.jpg b/src/images/photos/3.jpg deleted file mode 100644 index 5e40be94d2..0000000000 Binary files a/src/images/photos/3.jpg and /dev/null differ diff --git a/src/images/photos/4.jpg b/src/images/photos/4.jpg deleted file mode 100644 index de64313343..0000000000 Binary files a/src/images/photos/4.jpg and /dev/null differ diff --git a/src/images/photos/5.jpg b/src/images/photos/5.jpg deleted file mode 100644 index aad06bff54..0000000000 Binary files a/src/images/photos/5.jpg and /dev/null differ diff --git a/src/images/photos/6.jpg b/src/images/photos/6.jpg deleted file mode 100644 index 104ff18faa..0000000000 Binary files a/src/images/photos/6.jpg and /dev/null differ diff --git a/src/images/photos/footer-girl-lg-1920.webp b/src/images/photos/footer-girl-lg-1920.webp new file mode 100644 index 0000000000..87216b8817 Binary files /dev/null and b/src/images/photos/footer-girl-lg-1920.webp differ diff --git a/src/images/photos/footer-girl-lg.webp b/src/images/photos/footer-girl-lg.webp new file mode 100644 index 0000000000..46ded6fbf4 Binary files /dev/null and b/src/images/photos/footer-girl-lg.webp differ diff --git a/src/images/photos/footer-girl-mobile.webp b/src/images/photos/footer-girl-mobile.webp new file mode 100644 index 0000000000..e84682899a Binary files /dev/null and b/src/images/photos/footer-girl-mobile.webp differ diff --git a/src/images/photos/gallery-girl-1-mobile.webp b/src/images/photos/gallery-girl-1-mobile.webp new file mode 100644 index 0000000000..f1ff10abca Binary files /dev/null and b/src/images/photos/gallery-girl-1-mobile.webp differ diff --git a/src/images/photos/gallery-girl-1.webp b/src/images/photos/gallery-girl-1.webp new file mode 100644 index 0000000000..289958921f Binary files /dev/null and b/src/images/photos/gallery-girl-1.webp differ diff --git a/src/images/photos/gallery-girl-2-mobile.webp b/src/images/photos/gallery-girl-2-mobile.webp new file mode 100644 index 0000000000..3866ddb58f Binary files /dev/null and b/src/images/photos/gallery-girl-2-mobile.webp differ diff --git a/src/images/photos/gallery-girl-2.webp b/src/images/photos/gallery-girl-2.webp new file mode 100644 index 0000000000..4dfb4c0050 Binary files /dev/null and b/src/images/photos/gallery-girl-2.webp differ diff --git a/src/images/photos/gallery-girl-3-mobile.webp b/src/images/photos/gallery-girl-3-mobile.webp new file mode 100644 index 0000000000..da8eb84150 Binary files /dev/null and b/src/images/photos/gallery-girl-3-mobile.webp differ diff --git a/src/images/photos/gallery-girl-3.webp b/src/images/photos/gallery-girl-3.webp new file mode 100644 index 0000000000..e58fe84faf Binary files /dev/null and b/src/images/photos/gallery-girl-3.webp differ diff --git a/src/images/photos/gallery-speaker-4-mobile.webp b/src/images/photos/gallery-speaker-4-mobile.webp new file mode 100644 index 0000000000..8586655123 Binary files /dev/null and b/src/images/photos/gallery-speaker-4-mobile.webp differ diff --git a/src/images/photos/gallery-speaker-4.webp b/src/images/photos/gallery-speaker-4.webp new file mode 100644 index 0000000000..74da68af2e Binary files /dev/null and b/src/images/photos/gallery-speaker-4.webp differ diff --git a/src/images/photos/gallery-sunglasses-5.webp b/src/images/photos/gallery-sunglasses-5.webp new file mode 100644 index 0000000000..c4da72908d Binary files /dev/null and b/src/images/photos/gallery-sunglasses-5.webp differ diff --git a/src/images/photos/gallery-sunglasses-6-mobile.webp b/src/images/photos/gallery-sunglasses-6-mobile.webp new file mode 100644 index 0000000000..8ee6a7baea Binary files /dev/null and b/src/images/photos/gallery-sunglasses-6-mobile.webp differ diff --git a/src/images/photos/gallery-sunglasses-6.webp b/src/images/photos/gallery-sunglasses-6.webp new file mode 100644 index 0000000000..60e85028a0 Binary files /dev/null and b/src/images/photos/gallery-sunglasses-6.webp differ diff --git a/src/images/photos/hero-girl-lg.webp b/src/images/photos/hero-girl-lg.webp new file mode 100644 index 0000000000..8999637074 Binary files /dev/null and b/src/images/photos/hero-girl-lg.webp differ diff --git a/src/images/photos/hero-girl-md.webp b/src/images/photos/hero-girl-md.webp new file mode 100644 index 0000000000..5a0d635370 Binary files /dev/null and b/src/images/photos/hero-girl-md.webp differ diff --git a/src/images/photos/hero-girl-mobile.webp b/src/images/photos/hero-girl-mobile.webp new file mode 100644 index 0000000000..73f4d23fa4 Binary files /dev/null and b/src/images/photos/hero-girl-mobile.webp differ diff --git a/src/images/photos/products-color-bt-speaker.webp b/src/images/photos/products-color-bt-speaker.webp new file mode 100644 index 0000000000..a1ebc9bc7e Binary files /dev/null and b/src/images/photos/products-color-bt-speaker.webp differ diff --git a/src/images/photos/products-portable-smart-speaker.webp b/src/images/photos/products-portable-smart-speaker.webp new file mode 100644 index 0000000000..9748ae5bfb Binary files /dev/null and b/src/images/photos/products-portable-smart-speaker.webp differ diff --git a/src/images/photos/products-soundlink-bt-speaker.webp b/src/images/photos/products-soundlink-bt-speaker.webp new file mode 100644 index 0000000000..4e678fe82d Binary files /dev/null and b/src/images/photos/products-soundlink-bt-speaker.webp differ diff --git a/src/scripts/main.js b/src/scripts/main.js index ad9a93a7c1..0c820e0485 100644 --- a/src/scripts/main.js +++ b/src/scripts/main.js @@ -1 +1,360 @@ 'use strict'; + +// 1. Selectors +// 2. Helper functions +// 2.1. Header scrolled state +// 2.2. Toggle Menu +// 2.3. Focus Trap & Keyboard Handler +// 2.4. Proximity detection for burger/X +// 2.5. Scroll Lock +// 2.6. Soundwave icon animation +// 3. Handlers +// 3.1. Handle burger click +// 3.2. Submit handling (mock/dummy) +// 4. Init + +// ================================================= +// SELECTORS +// ================================================= + +const burger = document.querySelector('.burger'); +const header = document.querySelector('.header'); +const menu = document.querySelector('.menu'); +const menuLinks = document.querySelectorAll('.menu__link'); +const menuOverlay = document.querySelector('.menu-overlay'); +const contactForm = document.querySelector('.contact__form'); + +// ================================================= +// HELPER FUNCTIONS +// ================================================= + +const getFocusableElements = () => { + return menu.querySelectorAll( + 'a[href], button:not([disabled]), input, textarea, select, [tabindex]:not([tabindex="-1"])' + ); +}; + +// ------------------------------------------------- +// HEADER SCROLLED STATE (throttle) +// ------------------------------------------------- + +let ticking = false; + +const handleScroll = () => { + if (!ticking) { + ticking = true; + window.requestAnimationFrame(() => { + if (window.scrollY > 50) { + header.classList.add('is-scrolled'); + } else { + header.classList.remove('is-scrolled'); + } + ticking = false; + }); + } +}; + +// ------------------------------------------------- +// TOGGLE MENU +// ------------------------------------------------- + +const toggleMenu = (forceClose = false) => { + const isOpen = forceClose ? true : burger.getAttribute('aria-expanded') === 'true'; + const newState = !isOpen; + + burger.setAttribute('aria-expanded', String(newState)); + menu.classList.toggle('is-open', newState); + document.body.classList.toggle('menu-is-open', newState); + header.classList.toggle('menu-is-open', newState); + + toggleScrollLock(newState); + + if (newState) { + document.addEventListener('keydown', handleFocusTrap); + + requestAnimationFrame(() => { + const focusable = getFocusableElements(); + focusable[0]?.focus(); + }); + } else { + document.removeEventListener('keydown', handleFocusTrap); + } +}; + +// ------------------------------------------------- +// FOCUS TRAP & KEYBOARD HANDLER +// ------------------------------------------------- + +const handleFocusTrap = (e) => { + const focusable = getFocusableElements(); + const firstElement = focusable[0]; + const lastElement = focusable[focusable.length - 1]; + +// ------------------------------------------------- +// ESC key handle (closing) +// ------------------------------------------------- + + if (e.key === 'Escape') { + toggleMenu(true); + burger.focus(); // add focus to open menu button + + return; + } + +// ------------------------------------------------- +// TAB key handle +// ------------------------------------------------- + + if (e.key === 'Tab') { + if (e.shiftKey) { + if (document.activeElement === firstElement) { + e.preventDefault(); + lastElement.focus(); + } + } else { + if (document.activeElement === lastElement) { + e.preventDefault(); + firstElement.focus(); + } + } + } +}; + +// ------------------------------------------------- +// PROXIMITY DETECTION for BURGER/X +// ------------------------------------------------- + +const initProximitySignal = () => { + if (!burger) return; + + if ('ontouchstart' in window || navigator.maxTouchPoints > 0) { + return; + } + + let ticking = false; + + window.addEventListener('mousemove', (e) => { + if (!ticking) { + window.requestAnimationFrame(() => { + const rect = burger.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + const distance = Math.hypot(e.clientX - centerX, e.clientY - centerY); + + if (burger.getAttribute('aria-expanded') === 'true' && distance < 200) { + burger.classList.add('is-nearby'); + } else { + burger.classList.remove('is-nearby'); + } + ticking = false; + }); + ticking = true; + } + }); +}; + +// ------------------------------------------------- +// SCROLL LOCK +// ------------------------------------------------- + +// const toggleScrollLock = (isLocked) => { +// const body = document.body; + +// if (isLocked) { +// const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth; +// body.style.paddingRight = `${scrollBarWidth}px`; +// body.classList.add('scroll-locked'); +// } else { +// body.style.paddingRight = ''; +// body.classList.remove('scroll-locked'); +// } +// }; + +// ------------------------------------------------- +// trying to compensate scrollbar +// ------------------------------------------------- + +const toggleScrollLock = (isLocked) => { + const body = document.body; + const html = document.documentElement; + + if (isLocked) { + const scrollBarWidth = window.innerWidth - html.clientWidth; + + // --- disable header transitions --- + if (header) { + header.style.transition = 'none'; + } + + // --- lock scroll --- + html.style.overflow = 'hidden'; + body.style.overflow = 'hidden'; + + // --- only compensate if scrollbar exists --- + if (scrollBarWidth > 0) { + body.style.paddingRight = `${scrollBarWidth}px`; + + if (header) { + header.style.paddingRight = `${scrollBarWidth}px`; + } + } + + // --- force reflow --- + if (header) { + void header.offsetHeight; + } + + // --- restore header transitions --- + if (header) { + header.style.transition = ''; + } + + body.classList.add('scroll-locked'); + } else { + + // --- disable header transitions on unlock --- + if (header) { + header.style.transition = 'none'; + } + + // --- unlock scroll --- + html.style.overflow = ''; + body.style.overflow = ''; + body.style.paddingRight = ''; + + if (header) { + header.style.paddingRight = ''; + } + + // --- force reflow on unlock --- + if (header) { + void header.offsetHeight; + } + + // --- restore header transitions --- + if (header) { + header.style.transition = ''; + } + + body.classList.remove('scroll-locked'); + } +}; + +// ------------------------------------------------- +// FISHER-YATES helper +// ------------------------------------------------- + +function fisherYatesShuffle(array) { + const shuffled = [...array]; + + for (let i = shuffled.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; + } + + return shuffled; +} + +// ------------------------------------------------- +// SOUNDWAVE icon ANIMATION (TODO: add rAF) +// ------------------------------------------------- + +const animateSoundwave = () => { + const bars = document.querySelectorAll('.wave-bar'); + const barsArray = Array.from(bars); + if (!barsArray.length) { + return; + } + + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + return; + } + + const shuffled = fisherYatesShuffle(barsArray); + const selected = []; + const minDistance = 2; + const maxBars = 4; + + for (const candidate of shuffled) { + if (selected.length >= maxBars) break; + const candIdx = barsArray.indexOf(candidate); + const tooClose = selected.some(s => Math.abs(barsArray.indexOf(s) - candIdx) < minDistance); + if (!tooClose) selected.push(candidate); + } + + selected.forEach((bar, index) => { + const staggerDelay = (index * 0.15 + Math.random() * 0.2).toFixed(2) + 's'; + const duration = (Math.random() * 1 + 1.5).toFixed(2) + 's'; + const randomOpacity = (Math.random() * 0.6 + 0.1).toFixed(2); + + bar.style.setProperty('--target-opacity', randomOpacity); + bar.style.setProperty('--delay', staggerDelay); + bar.style.setProperty('--duration', duration); + + setTimeout(() => { + bar.classList.add('is-animating'); + }, 10); + }); +}; + +// ================================================= +// HANDLERS +// ================================================= + +const handleBurgerClick = () => toggleMenu(); + +const handleLinkClick = (e) => { + toggleMenu(true); +}; + +// ------------------------------------------------- +// SUBMIT HANDLING (mock/dummy) +// ------------------------------------------------- + +const handleFormSubmit = (e) => { + e.preventDefault(); + + const submitBtn = contactForm.querySelector('.contact__submit'); + + submitBtn.textContent = 'Sending...'; + submitBtn.disabled = true; + + setTimeout(() => { + submitBtn.textContent = 'Sent!'; + contactForm.reset(); + + setTimeout(() => { + submitBtn.textContent = 'Send'; + submitBtn.disabled = false; + }, 2000); + }, 1500); +}; + +// ================================================= +// INIT +// ================================================= + +const init = () => { + if (burger) { + burger.addEventListener('click', handleBurgerClick); + } + + if (menuOverlay) { + menuOverlay.addEventListener('click', () => toggleMenu(true)); + } + + if (menuLinks) { + menuLinks.forEach(link => link.addEventListener('click', handleLinkClick)); + } + + if (contactForm) { + contactForm.addEventListener('submit', handleFormSubmit); + } + + initProximitySignal(); + animateSoundwave(); + + window.addEventListener('scroll', handleScroll); +}; + +document.addEventListener('DOMContentLoaded', init); diff --git a/src/styles/_fonts.scss b/src/styles/_fonts.scss deleted file mode 100644 index 45cdd54008..0000000000 --- a/src/styles/_fonts.scss +++ /dev/null @@ -1,6 +0,0 @@ -@font-face { - font-family: Roboto, Arial, Helvetica, sans-serif; - src: url('../fonts/Roboto-Regular-webfont.woff') format('woff'); - font-weight: normal; - font-style: normal; -} diff --git a/src/styles/_typography.scss b/src/styles/_typography.scss deleted file mode 100644 index 1837eb46e2..0000000000 --- a/src/styles/_typography.scss +++ /dev/null @@ -1,3 +0,0 @@ -h1 { - @extend %h1; -} diff --git a/src/styles/_utils.scss b/src/styles/_utils.scss deleted file mode 100644 index 3280c3fe10..0000000000 --- a/src/styles/_utils.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import 'utils/vars'; -@import 'utils/mixins'; -@import 'utils/extends'; diff --git a/src/styles/abstracts/_functions.scss b/src/styles/abstracts/_functions.scss new file mode 100644 index 0000000000..aa1ce03177 --- /dev/null +++ b/src/styles/abstracts/_functions.scss @@ -0,0 +1,35 @@ +@use 'sass:math'; + +// ================================================= +// CALCULATE PX to REM +// ================================================= + +@function rem($px) { + @return math.div($px, 16) * 1rem; +} + +// ================================================= +// CALCULATE PX to VW +// ================================================= + +@function fluid-calc( + $min-size, + $max-size, + $min-width: 320px, + $max-width: 1440px +) { + $min-size-val: math.div($min-size, $min-size * 0 + 1); + $max-size-val: math.div($max-size, $max-size * 0 + 1); + $min-width-val: math.div($min-width, $min-width * 0 + 1); + $max-width-val: math.div($max-width, $max-width * 0 + 1); + $slope: math.div( + $max-size-val - $min-size-val, + $max-width-val - $min-width-val + ); + $intercept: $min-size-val - ($min-width-val * $slope); + + @return max( + #{$min-size}, + min(calc(#{$slope * 100}vw + #{$intercept}px), #{$max-size}) + ); +} diff --git a/src/styles/abstracts/_mixins.scss b/src/styles/abstracts/_mixins.scss new file mode 100644 index 0000000000..3f5d4000eb --- /dev/null +++ b/src/styles/abstracts/_mixins.scss @@ -0,0 +1,19 @@ +@use 'vars' as *; +@use 'sass:map'; + +// todo button hover +@mixin hover($_property, $_toValue) { + transition: #{$_property} var(--transition-normal); + &:hover { + #{$_property}: $_toValue; + } +} + +// media queries +@mixin mq($size) { + $width: map.get($breakpoints, $size); + + @media (min-width: $width) { + @content; + } +} diff --git a/src/styles/abstracts/_placeholders.scss b/src/styles/abstracts/_placeholders.scss new file mode 100644 index 0000000000..040df3b252 --- /dev/null +++ b/src/styles/abstracts/_placeholders.scss @@ -0,0 +1,21 @@ +@use '../abstracts/vars' as *; + +%heading-base { + color: var(--txt-primary); + font-family: #{$font-primary}; + font-weight: var(--wght-bold); + letter-spacing: var(--l-spacing-tight); +} + +%link-base { + color: var(--txt-primary); + font-family: #{$font-primary}; + text-decoration: none; +} + +%paragraph-base { + color: var(--txt-primary); + font-family: #{$font-primary}; + font-weight: var(--wght-normal); + font-size: var(--fs-s); +} diff --git a/src/styles/abstracts/_vars.scss b/src/styles/abstracts/_vars.scss new file mode 100644 index 0000000000..caec372a37 --- /dev/null +++ b/src/styles/abstracts/_vars.scss @@ -0,0 +1,66 @@ +// ================================================== +// Global design tokens +// ================================================== + +// 1. Typography +// 1.1 Base settings +// 2. Colors (Raw palette) +// 2.1 Gray scale +// 2.2 Other colors +// 3. Breakpoints + +// ------------------------------------------------- +// 1. TYPOGRAPHY +// ------------------------------------------------- + +// --- 1.1 Base settings + +$font-format: format('woff2'); +$font-name: 'Roboto'; +$font-path: '../fonts'; +$font-primary: $font-name, Arial, Helvetica, sans-serif; +$font-secondary: $font-name, Arial, Helvetica, sans-serif; + +// --- Font weights --- + +// $wght-normal: 400; +// $wght-medium: 500; +// $wght-bold: 700; +// $wght-black: 900; + +// ------------------------------------------------- +// 2. COLORS (Raw palette) +// ------------------------------------------------- + +// --- 2.1 Gray scale --- + +$clr-white: #fafafa; +$clr-gray-100: #eee; // bg +$clr-gray-200: #ececec; // form input bg +$clr-gray-250: #e9e9e9; // form input hover +$clr-gray-300: #cccbcb; // form borders, separators +$clr-gray-400: #919191; // placeholders +$clr-gray-450: #999; // disabled +$clr-gray-500: #5a5a5a; // description, metadata +$clr-gray-600: #3d3d3d; // nav link hover +$clr-gray-800: #292929; // headings, body, paragraphs +$clr-gray-900: #1f1f1f; // form input hover border +$clr-gray-950: #111; // button pressed, dark accents (form input focus) + +// --- 2.2 Other colors --- + +$clr-red-500: #ef4444; // error +$clr-green-600: #209922; // success +$clr-slate-100: #e2e8f0; + +// ------------------------------------------------- +// 3. BREAKPOINTS +// ------------------------------------------------- + +$breakpoints: ( + 'sm': 576px, + 'md': 768px, + 'lg': 1024px, + 'xl': 1280px, + 'xxl': 1440px, +); diff --git a/src/styles/abstracts/index.scss b/src/styles/abstracts/index.scss new file mode 100644 index 0000000000..e38beaff5f --- /dev/null +++ b/src/styles/abstracts/index.scss @@ -0,0 +1,4 @@ +@forward 'vars'; +@forward 'functions'; +@forward 'mixins'; +@forward 'placeholders'; diff --git a/src/styles/base/_fonts.scss b/src/styles/base/_fonts.scss new file mode 100644 index 0000000000..cddf1c5deb --- /dev/null +++ b/src/styles/base/_fonts.scss @@ -0,0 +1,33 @@ +@use '../abstracts' as *; + +@font-face { + font-family: #{$font-name}; + src: url('#{$font-path}/roboto-regular-webfont.woff2') #{$font-format}; + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: #{$font-name}; + src: url('#{$font-path}/roboto-medium-webfont.woff2') #{$font-format}; + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: #{$font-name}; + src: url('#{$font-path}/roboto-bold-webfont.woff2') #{$font-format}; + font-weight: 700; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: #{$font-name}; + src: url('#{$font-path}/roboto-blackitalic-webfont.woff2') #{$font-format}; + font-weight: 900; + font-style: italic; + font-display: swap; +} diff --git a/src/styles/base/_layout.scss b/src/styles/base/_layout.scss new file mode 100644 index 0000000000..2dd3ced80b --- /dev/null +++ b/src/styles/base/_layout.scss @@ -0,0 +1,36 @@ +@use '../abstracts' as *; + +.grid-container { + width: 100%; + max-width: var(--page-content-width); + padding: 0 var(--page-side-spacing-s); + margin: 0 auto; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--gutter); + + @include mq('md') { + padding: 0 var(--page-side-spacing-md); + grid-template-columns: repeat(6, 1fr); + } + + @include mq('lg') { + padding: 0 var(--page-side-spacing-lg); + grid-template-columns: repeat(12, 1fr); + } +} + +// ------------------------------------------------- +// SECTION +// ------------------------------------------------- + +.section { + padding-top: var(--section-spacing); + + &__title { + grid-column: 1 / -1; + text-align: center; + line-height: var(--lh-section-title); + padding-bottom: var(--page-title-padding); + } +} diff --git a/src/styles/base/_reset.scss b/src/styles/base/_reset.scss new file mode 100644 index 0000000000..e4b732f84b --- /dev/null +++ b/src/styles/base/_reset.scss @@ -0,0 +1,79 @@ +@use '/src/styles/abstracts/' as *; + +*, +*::before, +*::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; + -webkit-text-size-adjust: 100%; +} + +body { + background: var(--bg-page); + line-height: var(--lh-base); + min-height: 100dvh; + overflow-x: hidden; + font-family: #{$font-primary}; +} + +button, +input, +textarea, +select, +a { + font-family: inherit; +} + +ul, +ol { + list-style: none; + margin: 0; +} + +h1, +h2, +h3, +h4, +h5, +h6, +p { + overflow-wrap: break-word; + margin: 0; +} + +:focus-visible { + outline: 2px solid var(--focus-border-clr); + outline-offset: 4px; + + // border-radius: 3px; +} + +img, +picture, +video, +canvas, +svg { + display: block; + + // max-width: 100%; +} + +// --- SEO: TODO h1 class="visually-hidden" --- + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + clip-path: inset(50%); + white-space: nowrap; + border: 0; +} diff --git a/src/styles/base/_root.scss b/src/styles/base/_root.scss new file mode 100644 index 0000000000..ee4528924c --- /dev/null +++ b/src/styles/base/_root.scss @@ -0,0 +1,262 @@ +@use '../abstracts/vars' as *; +@use '../abstracts/functions' as *; +@use '../abstracts/mixins' as *; + +// ================================================= +// CSS Custom Properties +// ================================================= + +// 1. Status & borders +// 2. Background & surface +// 3. Typography +// 3.1. Font sizes +// 3.2. Line heights +// 3.3. Font weights +// 3.4. Letter spacing +// 4. Grid container +// 5. Header +// 6. Navbar +// 6.1. Burger +// 6.1. Menu +// 7. Hero +// 8. Form +// 9. Transitions + +:root { + section[id] { + scroll-margin-top: calc( + var(--header-padding) + var(--burger-icon-size) + ); // section header visible under the fixed header + + // @include mq('md') { + // scroll-margin-top: calc( + // var(--header-padding-md) + var(--burger-icon-size) + // ); + // } + + @include mq('lg') { + scroll-margin-top: calc( + var(--header-padding-lg) + var(--burger-icon-size) + ); + } + } + + // ------------------------------------------------- + // STATUS & BORDERS + // ------------------------------------------------- + + --focus-border-clr: #{$clr-gray-800}; + --status-error: #{$clr-red-500}; // TODO + --status-success: #{$clr-green-600}; // TODO + + // ------------------------------------------------- + // BACKGROUND & SURFACE + // ------------------------------------------------- + + --bg-page: #{$clr-white}; + --bg-section: #{$clr-white}; + + // ------------------------------------------------- + // TYPOGRAPHY + // ------------------------------------------------- + + --txt-primary: #{$clr-gray-800}; // headings, body, paragraphs + --txt-secondary: #{$clr-gray-500}; // description, metadata + --txt-muted: #{$clr-gray-400}; // placeholders + --txt-hover: #{$clr-gray-500}; + --txt-on-dark: #{$clr-white}; + + // --- Font sizes --- + + --fs-base: 1rem; + --fs-xs: clamp(14px 0.9em 15px); + --fs-s: clamp(16px, 1.1em, 18px); + --fs-md: clamp(20px, 2vw, 22px); + --fs-lg: #{fluid-calc(22px, 30px)}; + --fs-xl: clamp(28px, 5vw, 40px); + + // --- Line heights --- + + --lh-base: 1.7; + --lh-section-title: 1.2; + --lh-tight: 1; + + // --- Font weights --- + + --wght-normal: 400; + --wght-medium: 500; + --wght-bold: 700; + --wght-black: 900; + + // --- Letter spacing --- + + --l-spacing-tight: -0.02em; + --l-spacing-medium: 0.04em; + --l-spacing-wide: 0.09em; + + // ------------------------------------------------- + // GRID-CONTAINER + // ------------------------------------------------- + + --page-content-width: #{rem(1680)}; + --gutter: #{fluid-calc(12px, 24px)}; + --page-side-spacing-s: #{rem(24)}; + --page-side-spacing-md: #{rem(72)}; + --page-side-spacing-lg: #{rem(120)}; + --section-spacing: #{fluid-calc(64px, 120px)}; + --page-title-padding: clamp(1.5rem, 4vh, 4rem); + --dynamic-margin: max( + var(--page-side-spacing-lg), + calc((100vw - var(--page-content-width)) / 2 + var(--page-side-spacing-lg)) + ); // grid mirroring + + // ------------------------------------------------- + // HEADER + // ------------------------------------------------- + + --header-actions-gap: #{fluid-calc(4px, 20px)}; + --header-bg-hover: #{$clr-white}; + --header-bg: transparent; + --header-font-size: var(--fs-s); + --header-logo-height: #{fluid-calc(120px, 150px)}; + --header-padding-lg: #{rem(10)} 0 #{rem(10)}; + --header-padding: #{rem(12)} 0 #{rem(12)}; + --header-phone-icon-width: #{fluid-calc(20px, 24px)}; + --header-phone-width: #{fluid-calc(170px, 180px)}; + --header-shadow-hover: 0 4px 30px rgba(0, 0, 0, 0.05); + + // ------------------------------------------------- + // NAVBAR + // ------------------------------------------------- + + // --- Burger --- + + --burger-before-size: #{rem(44)}; + --burger-icon-size: #{fluid-calc(18px, 22px)}; + --burger-touch-target: #{rem(48)}; + --burger-transition: var(--transition-fast); + + // --- Menu --- + + --nav-bg: #{$clr-slate-100}; + --nav-link-color: var(--txt-primary); + --nav-link-font-size: var(--fs-md); + --nav-link-hover: #{$clr-gray-600}; + --nav-link-transition: var(--duration-fast) var(--easing-ease-in); + --nav-link-weight: var(--wght-bold); + --nav-list-gap: #{rem(24)}; + --nav-list-padding: #{fluid-calc(40px, 54px)}; + --nav-scrollbar-clr: #{$clr-gray-400}; + --nav-spacing: var(--l-spacing-wide); + --nav-transition: var(--transition-normal); + --nav-overlay-fallback: #{rgba(#000, 0.7)}; + --nav-overlay: linear-gradient( + to right, + #{rgba(#000, 0.65)} 0%, + #{rgba(#000, 0.35)} 32%, + #{rgba(#000, 0.08)} 66%, + transparent 100% + ); + --nav-overlay-md: linear-gradient( + to right, + #{rgba(#000, 0.5)} 0%, + #{rgba(#000, 0.3)} 4%, + #{rgba(#000, 0.08)} 60%, + transparent 100% + ); + + // ------------------------------------------------- + // HERO + // ------------------------------------------------- + + --hero-container-padding: 2.5rem; + --hero-content-gap: #{fluid-calc(16px, 40px)}; + --hero-font-size: #{fluid-calc(34px, 60px)}; + --hero-icon-shift: 0.3rem; + --hero-icon-width: #{fluid-calc(54px, 96px)}; + --hero-gradient-overlay: linear-gradient( + to top, + #{rgba($clr-white, $alpha: 0.2)} 0%, + #{rgba($clr-white, $alpha: 0.1)} 50%, + transparent 85% + ); + + // ------------------------------------------------- + // PRODUCTS + // ------------------------------------------------- + + --card-category-clr: #{$clr-gray-500}; + --card-img-width: #{fluid-calc(200px, 250px)}; + --card-shadow-width: #{rem(100)}; + + // ------------------------------------------------- + // GALLERY + // ------------------------------------------------- + + --gallery-border: inset 0 0 0 1px rgba(0, 0, 0, 0.08); + --gallery-transition: var(--duration-smooth) var(--easing-smooth); + --gallery-hover: inset 0 0 0 1px rgba(0, 0, 0, 0.12), + 0 12px 24px rgba(0, 0, 0, 0.12); + + // ------------------------------------------------- + // BUTTON + // ------------------------------------------------- + + --btn-bg: #{$clr-gray-800}; + --btn-active: #{$clr-gray-950}; + --btn-disabled: #{$clr-gray-450}; + --btn-disabled-txt: #d0d0d0; + --btn-radius: #{rem(40)}; + --btn-hover-bg: #{$clr-white}; + --btn-hover-txt: #{$clr-gray-800}; + + // ------------------------------------------------- + // CONTACT + // ------------------------------------------------- + + --contact-info-gap: #{fluid-calc(1.4rem, 2rem)}; + --contact-info-label: var(--fs-xs); + --form-btn-width: #{rem(600)}; + --form-input-bg: #{$clr-gray-200}; + --form-input-border: inset 1 1 0 0px rgba(0, 0, 0, 0.08); + --form-input-error: var(--status-error); // TODO/optional + --form-input-focus: #{$clr-gray-950}; // form border focus + --form-input-hover: #{$clr-gray-250}; + --form-input-placeholder: var(--txt-muted); + --form-input-success: var(--status-success); // TODO/optional + --form-input-txt: var(--txt-primary); + --form-label: var(--txt-primary); + --form-padding-y: calc(var(--form-radius) / 2); + --form-radius: #{fluid-calc(28px, 32px)}; + --padding-block: var(--form-radius); + --padding-inline: var(--form-radius); + + // ------------------------------------------------- + // TRANSITION TOKENS + // ------------------------------------------------- + + --transition-fast: var(--duration-fast) var(--easing-linear); + --transition-normal: var(--duration-normal) var(--easing-ease-in); + --transition-subtle: var(--duration-slow) var(--easing-subtle); // product card img + --transition-smooth: var(--duration-smooth) var(--easing-smooth) + var(--delay-normal); + + // --- DURATIONS --- + + --duration-fast: 0.25s; // micro-interactions, borders, buttons + --duration-normal: 0.35s; // cards, medium elements, menu sidebar + --duration-slow: 0.7s; // emphasis (products section) + --duration-smooth: 0.5s; // galleries, pictures + + // --- EASINGS --- + + --easing-ease-in: cubic-bezier(0.4, 0, 0.2, 1); + --easing-linear: ease; + --easing-smooth: cubic-bezier(0.25, 0.46, 0.45, 0.94); + --easing-subtle: cubic-bezier(0.15, 0, 0.3, 1); + + // --- DELAYS --- + + --delay-long: 0.2s; + --delay-normal: 0.1s; +} diff --git a/src/styles/base/_typography.scss b/src/styles/base/_typography.scss new file mode 100644 index 0000000000..86ac517f74 --- /dev/null +++ b/src/styles/base/_typography.scss @@ -0,0 +1,24 @@ +@use '../abstracts' as *; + +h2 { + @extend %heading-base; + + font-size: var(--fs-xl); +} + +h3 { + @extend %heading-base; + + font-size: var(--fs-lg); +} + +p { + @extend %paragraph-base; +} + +li { + @extend %paragraph-base; + + font-size: var(--fs-s); + font-weight: var(--wght-medium); +} diff --git a/src/styles/base/index.scss b/src/styles/base/index.scss new file mode 100644 index 0000000000..add8b94a62 --- /dev/null +++ b/src/styles/base/index.scss @@ -0,0 +1,5 @@ +@forward 'reset'; +@forward 'fonts'; +@forward 'root'; +@forward 'typography'; +@forward 'layout'; diff --git a/src/styles/components/_burger.scss b/src/styles/components/_burger.scss new file mode 100644 index 0000000000..7412fdcebd --- /dev/null +++ b/src/styles/components/_burger.scss @@ -0,0 +1,155 @@ +@use '../abstracts' as *; +@use '../base' as *; + +.burger { + width: var(--burger-touch-target); + height: var(--burger-touch-target); + flex-shrink: 0; + border: none; + background: transparent; + cursor: pointer; + position: relative; + display: flex; + align-items: center; + justify-content: center; + z-index: 201; + + // ------------------------------------------------- + // LOCATOR for close-x (a11y) + // ------------------------------------------------- + + &::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0); + width: var(--burger-before-size); + height: var(--burger-before-size); + background-color: rgba($clr-white, 0.2); + border-radius: 50%; + transition: + transform var(--burger-transition), + opacity var(--burger-transition); + opacity: 0; + pointer-events: none; + } + + &[aria-expanded='true']::before { + @include mq('md') { + transform: translate(-50%, -50%) scale(1); + background-color: rgba($clr-white, 0.3); + opacity: 1; + } + } + + // ------------------------------------------------- + // RIPPLE PROXIMITY EFFECT for close-x (a11y) + // ------------------------------------------------- + + &::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 60px; + height: 60px; + border: 1px solid rgba($clr-white, 0.6); + border-radius: 50%; + transform: translate(-50%, -50%) scale(0); + opacity: 0; + pointer-events: none; + transition: + transform 0.6s ease, + opacity 0.6s ease; + } + + &.is-nearby[aria-expanded='true']::after { + animation: ripple-hint 2s infinite; + } + + // ------------------------------------------------- + // BURGER SIZE & TOGGLE + // ------------------------------------------------- + + &__wrapper { + position: relative; + width: var(--burger-icon-size); + height: var(--burger-icon-size); + display: flex; + align-items: center; + justify-content: center; + } + + &__icon { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 100%; + height: 100%; + object-fit: contain; + transition: + opacity var(--burger-transition), + visibility var(--burger-transition), + transform var(--burger-transition); + opacity: 0; + visibility: hidden; + + &--default { + .burger[aria-expanded='false'] & { + opacity: 1; + visibility: visible; + } + } + + &--hover { + @media (hover: hover) { + .burger[aria-expanded='false']:hover & { + opacity: 1; + visibility: visible; + } + } + } + + &--close { + .burger[aria-expanded='true'] & { + opacity: 1; + visibility: visible; + } + } + } + + &[aria-expanded='true']:hover &__icon--close { + transform: translate(-50%, -50%) scale(1.1); + } + + &[aria-expanded='true']:active &__icon--close { + transform: translate(-50%, -50%) scale(0.94); + } + + @media (hover: hover) { + &[aria-expanded='false']:hover &__icon--default { + opacity: 0; + visibility: hidden; + } + } +} + +// --- RIPPLE EFFECT animation --- + +@keyframes ripple-hint { + 0% { + transform: translate(-50%, -50%) scale(0.6); + opacity: 0; + } + + 50% { + opacity: 0.4; + } + + 100% { + transform: translate(-50%, -50%) scale(1.4); + opacity: 0; + } +} diff --git a/src/styles/components/_button-primary.scss b/src/styles/components/_button-primary.scss new file mode 100644 index 0000000000..284424f562 --- /dev/null +++ b/src/styles/components/_button-primary.scss @@ -0,0 +1,65 @@ +@use '../abstracts' as *; +@use '../base' as *; + +.btn-primary { + grid-column: 1 / -1; + grid-row: 5 / span 1; + margin: 0 auto; + display: flex; + justify-content: center; + align-items: center; + height: #{rem(56)}; + width: 100%; + background-color: var(--btn-bg); + border-radius: var(--btn-radius); + border: 2px solid var(--btn-bg); + color: var(--txt-on-dark); + font-weight: var(--wght-medium); + font-size: var(--fs-md); + letter-spacing: var(--l-spacing-medium); + text-decoration: none; + outline: none; + cursor: pointer; + transition: + color var(--transition-normal), + background-color var(--transition-normal); + + @include mq('xxl') { + max-width: var(--form-btn-width); + } + + @media (hover: hover) { + &:hover:not(:disabled) { + background-color: var(--btn-hover-bg); + color: var(--btn-hover-txt); + border: 2px solid var(--btn-hover-txt); + } + } + + // &:focus:not(:disabled) { + // outline: none; // prevents focus with mouse + // } + + &:focus-visible { + outline: 2px solid var(--focus-border-clr); + outline-offset: 4px; + } + + &:disabled { + background: var(--btn-disabled); + border-color: var(--btn-disabled); + color: var(--btn-disabled-txt); + cursor: not-allowed; + opacity: 0.7; + + &:hover { + background: var(--btn-disabled); + transform: none; + } + } + + &:active:not(:disabled) { + transform: scale(0.98); + background-color: var(--btn-active); + } +} diff --git a/src/styles/components/_card.scss b/src/styles/components/_card.scss new file mode 100644 index 0000000000..c5b6de793e --- /dev/null +++ b/src/styles/components/_card.scss @@ -0,0 +1,142 @@ +@use '../abstracts' as *; +@use '../base' as *; + +.product-card { + position: relative; + display: flex; + align-items: center; + flex-direction: column; + justify-content: flex-start; + background: var(--bg-page); + border-radius: #{rem(16)}; + height: 100%; + + // overflow: hidden; + + &::after { + content: ''; + position: absolute; + left: 50%; + transform: translateX(-50%); + z-index: 0; + width: clamp(180px, 65%, 300px); + height: 8%; + bottom: 32%; + opacity: 0.45; + background: radial-gradient( + ellipse 45% 25% at center, + rgba(0, 0, 0, 0.18) 0%, + rgba(0, 0, 0, 0.1) 40%, + transparent 100% + ), + radial-gradient( + ellipse 50% 45% at center, + rgba(0, 0, 0, 0.08) 0%, + rgba(0, 0, 0, 0.04) 50%, + transparent 100% + ), + radial-gradient( + ellipse 70% 55% at center, + rgba(0, 0, 0, 0.03) 0%, + transparent 100% + ); + transition: + opacity var(--transition-subtle), + transform var(--transition-subtle); + + // @include mq('lg') { + // bottom: 28%; + // } + + @include mq('xl') { + bottom: 40%; + } + + @media (min-width: 768px) and (max-width: 1023px) { + bottom: 30%; + } + } + + @media (hover: hover) { + &:hover::after { + opacity: 0.3; + transform: translateX(-50%) scaleX(0.92); + } + + &:hover &__image { + transform: translateY(-4px); + } + } + + &__image-wrapper { + position: relative; + width: #{fluid-calc(250px, 400px)}; + aspect-ratio: 1 / 1; + flex-shrink: 0; + + // overflow: hidden; + + @media (min-width: 768px) and (max-width: 1023px) and (orientation: portrait) { + width: #{fluid-calc(300px, 400px)}; + } + + @include mq('xl') { + width: #{fluid-calc(200px, 250px)}; + object-fit: contain; + } + } + + &__image { + z-index: 1; + width: 100%; + height: 100%; + object-fit: cover; + object-position: center 40%; + transform-origin: bottom; + scale: 1.08; + transition: transform var(--transition-subtle); + } + + &__info { + display: flex; + flex-direction: column; + width: 100%; + max-width: clamp(250px, 70%, 500px); + padding: 2rem 0 0.5rem; + text-align: left; + flex-grow: 1; + + @include mq('lg') { + padding: 2rem 0 0; + margin-top: 1rem; + } + + @include mq('xl') { + width: auto; + } + } + + &__name { + display: flex; + align-items: flex-start; + font-size: var(--fs-s); + line-height: 1.4; + margin-top: 1rem; + + @include mq('xl') { + min-height: 2.8em; + } + } + + &__category { + padding: 0.5rem 0 0.7rem; + color: var(--card-category-clr); + } + + &__price { + font-size: var(--fs-s); + font-weight: var(--wght-medium); + justify-content: flex-end; + margin-top: auto; + } +} diff --git a/src/styles/components/index.scss b/src/styles/components/index.scss new file mode 100644 index 0000000000..32bbcb3257 --- /dev/null +++ b/src/styles/components/index.scss @@ -0,0 +1,3 @@ +@forward 'burger'; +@forward 'button-primary'; +@forward 'card'; diff --git a/src/styles/layout/_contact.scss b/src/styles/layout/_contact.scss new file mode 100644 index 0000000000..8fdf51f61d --- /dev/null +++ b/src/styles/layout/_contact.scss @@ -0,0 +1,116 @@ +@use '../abstracts' as *; +@use '../base' as *; + +.contact { + &__wrapper { + grid-column: 1 / -1; + display: grid; + gap: calc(4 * var(--gutter)); + + @include mq('md') { + grid-template-columns: 2fr 1fr; + gap: calc(3 * var(--gutter)); + } + + @include mq('xl') { + grid-column: 1 / -1; + grid-template-columns: 1fr 1fr; + gap: calc(6 * var(--gutter)); + } + + @include mq('xxl') { + grid-column: 2 / -1; + } + + @media (min-width: 1024px) and (orientation: portrait) { + grid-column: 1 / -1; + grid-template-columns: 2fr 1fr; + } + } + + &__form { + display: flex; + flex-direction: column; + gap: var(--gutter); + } + + &__input { + @extend %paragraph-base; + + width: 100%; + padding: var(--form-padding-y) var(--padding-inline); + color: var(--form-input-txt); + background: var(--form-input-bg); + border: 2px solid transparent; + border-radius: var(--form-radius); + transition: + border-color var(--transition-fast), + background-color var(--transition-normal); + + &::placeholder { + color: var(--form-input-placeholder); + } + + @media (hover: hover) { + &:hover { + background-color: var(--form-input-hover); + box-shadow: var(--form-input-border); + } + } + + &:focus { + outline: none; + border-color: var(--form-input-focus); + } + + @include mq('lg') { + padding: var(--form-padding-y) var(--padding-inline); + } + + &--textarea { + padding: var(--padding-block) var(--padding-inline); + resize: vertical; + min-height: 10rem; + border-radius: var(--form-radius); + } + } + + &__submit { + margin-top: 1rem; + } + + &__info { + display: flex; + flex-direction: column; + gap: var(--contact-info-gap); + } + + &__info-item { + padding-left: var(--padding-inline); + + @include mq('md') { + padding-left: 0; + } + } + + &__info-label { + font-size: var(--contact-info-label); + color: var(--form-input-placeholder); + margin-bottom: 0.5rem; + } + + &__info-value { + @extend %paragraph-base; + + color: var(--form-input-txt); + text-decoration: none; + + &[href] { + transition: color 0.2 ease; + + &:hover { + color: var(--nav-link-hover); + } + } + } +} diff --git a/src/styles/layout/_footer.scss b/src/styles/layout/_footer.scss new file mode 100644 index 0000000000..7d85552528 --- /dev/null +++ b/src/styles/layout/_footer.scss @@ -0,0 +1,28 @@ +@use '../abstracts' as *; +@use '../base' as *; + +.footer { + &__image-wrapper { + width: 100%; + overflow: hidden; + } + + &__image { + width: 100%; + height: 100%; + height: auto; + display: block; + object-fit: cover; + object-position: center; + aspect-ratio: 1 / 1; + + @include mq('md') { + aspect-ratio: 4 / 3; + } + + @include mq('lg') { + aspect-ratio: 16 / 6; + object-position: center 55%; + } + } +} diff --git a/src/styles/layout/_gallery.scss b/src/styles/layout/_gallery.scss new file mode 100644 index 0000000000..6113b77959 --- /dev/null +++ b/src/styles/layout/_gallery.scss @@ -0,0 +1,121 @@ +@use '../abstracts' as *; + +.categories { + &__row { + grid-column: 1 / -1; + margin-bottom: 2rem; + + @include mq('md') { + margin-bottom: 4rem; + } + + // @include mq('lg') { + // grid-column: 1 / -1; + // } + + @include mq('xxl') { + grid-column: 2 / -2; + } + + @media (min-width: 840px) and (max-width: 1024px) and (orientation: portrait) { + grid-column: 1 / -1; + } + } + + &__images { + display: grid; + gap: var(--gutter); + grid-template-columns: 1fr 1fr; + + @include mq('md') { + .categories__row--type-a & { + grid-template-columns: 2fr 1fr; + } + + .categories__row--type-b & { + grid-template-columns: 1fr 2fr; + } + + .categories__row--type-c & { + grid-template-columns: 1fr 1fr; + } + } + } + + &__image-wrapper { + position: relative; + overflow: hidden; + height: auto; + + &::after { + content: ''; + position: absolute; + inset: 0; + box-shadow: var(--gallery-border); + transition: box-shadow var(--gallery-transition); + pointer-events: none; + } + + @include mq('md') { + height: clamp(200px, 28vw, 250px); + } + + @include mq('xl') { + height: #{rem(350)}; + } + } + + @media (hover: hover) { + &__image-wrapper:hover &__image { + transform: scale(1.02); + } + + &__image-wrapper:hover::after { + box-shadow: var(--gallery-hover); + } + } + + @media (prefers-reduced-motion: reduce) { + &__image { + transition: none; + } + + &__image-wrapper:hover &__image { + transform: none; + } + } + + &__image { + width: 100%; + height: 100%; + aspect-ratio: 1 / 1; + object-fit: cover; + object-position: center; + display: block; + transition: transform var(--gallery-transition); + + &[data-image='sunglasses-girl-mobile-crop'] { + @media (max-width: 820px) { + object-position: 75% center; + } + } + + &[data-image='earbuds-girl-mobile-crop'] { + object-position: 60% center; + } + } + + &__caption { + margin-top: clamp(0.5rem, 4vw, 1.3rem); + font-weight: var(--wght-medium); + } + + &__btn { + margin-top: calc(-1 * var(--gutter)); + max-width: #{rem(350)}; + + @include mq('md') { + margin-top: calc(-1 * (var(--gutter) * 2.5)); + } + } +} diff --git a/src/styles/layout/_header.scss b/src/styles/layout/_header.scss new file mode 100644 index 0000000000..a89b009c5a --- /dev/null +++ b/src/styles/layout/_header.scss @@ -0,0 +1,138 @@ +@use '../abstracts' as *; +@use '../base' as *; + +.header { + position: fixed; + top: 0; + left: 0; + width: 100%; + z-index: 200; + padding: var(--header-padding); + background: var(--header-bg); + transition: + background-color var(--transition-smooth), + backdrop-filter var(--transition-smooth), + padding var(--transition-smooth); + + @media (hover: hover) { + &:hover:not(.menu-is-open &) { + background-color: rgba($clr-white, 0.4); + backdrop-filter: blur(10px); + box-shadow: var(--header-shadow-hover); + } + } + + &.is-scrolled:not(.menu-is-open &) { + background-color: var(--header-bg-hover); + box-shadow: var(--header-shadow-hover); + } + + &__phone-icon { + width: var(--header-phone-icon-width); + height: auto; + flex-shrink: 0; + display: block; + z-index: 2; + pointer-events: none; + backface-visibility: hidden; + -webkit-font-smoothing: antialiased; + transform: translateZ(0); + transform-origin: center; + transition: transform var(--transition-normal) ease-out; + } + + &__phone-mask { + overflow: hidden; + display: block; + pointer-events: none; + } + + &__phone-number { + display: block; + opacity: 0; + width: var(--header-phone-width); + overflow: hidden; + white-space: nowrap; + color: var(--txt-primary); + font-weight: var(--wght-bold); + font-size: var(--header-font-size); + letter-spacing: var(--l-spacing-wide); + + // --- POSITIONING and ANIMATION --- + + transform: translateX(100%); + transition: + opacity var(--transition-smooth), + transform var(--transition-smooth); + + // --- RESPONSIVENESS --- + + @media (max-width: 1023px) { + display: none; + } + } + + &__phone-link { + display: flex; + align-items: center; + text-decoration: none; + padding: 0.5rem; + position: relative; + + @media (hover: hover) { + &:hover { + .header__phone-number { + opacity: 1; + transform: translateX(0); + } + } + } + } + + &.menu-is-open { + box-shadow: none !important; + backdrop-filter: none !important; + background-color: var(--header-bg) !important; + + .header__phone-link { + visibility: hidden; + } + } + + @include mq('lg') { + padding: var(--header-padding-lg); + } + + // ------------------------------------------------- + // MAIN CONTAINER + // ------------------------------------------------- + + &__container { + align-items: center; + } + + &__logo { + width: var(--header-logo-height); + max-width: #{rem(170)}; + margin-left: #{rem(-4)}; + position: relative; + align-self: center; + z-index: 201; + } + + &__actions { + display: flex; + gap: var(--header-actions-gap); + align-items: center; + justify-content: flex-end; + grid-column: 2 / span 1; + + @include mq('md') { + grid-column: 5 / span 2; + } + + @include mq('lg') { + grid-column: -2 / span 2; + } + } +} diff --git a/src/styles/layout/_hero.scss b/src/styles/layout/_hero.scss new file mode 100644 index 0000000000..fa828a0358 --- /dev/null +++ b/src/styles/layout/_hero.scss @@ -0,0 +1,135 @@ +@use '../abstracts' as *; +@use '../base' as *; + +.hero { + position: relative; + min-height: 100dvh; + display: flex; + align-items: flex-end; + padding-bottom: var(--hero-container-padding); + color: var(--txt-primary); + isolation: isolate; + overflow: hidden; + + @media (min-width: 512px) { + padding-bottom: calc(var(--hero-container-padding) + 4vw); + } + + @include mq('lg') { + padding-bottom: calc(var(--hero-container-padding) + 6vh); + } + + @media (min-width: 769px) and (max-width: 1024px) and (orientation: portrait) { + padding-bottom: calc(var(--hero-container-padding) + 5vw); + } + + // --- HERO BG COVER (heading readability) --- + + &::before { + position: absolute; + content: ''; + inset: 0; + background: var(--hero-gradient-overlay); + z-index: 1; + pointer-events: none; + } + + &__bg { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + object-position: center; + z-index: -1; + + @include mq('md') { + scale: 1.1; + } + + @include mq('lg') { + scale: 1; + } + } + + // ------------------------------------------------- + // H1 + ICON CONTAINER + // ------------------------------------------------- + + &__container { + position: relative; + z-index: 2; + } + + &__content { + display: flex; + flex-direction: column; + gap: var(--hero-content-gap); + grid-column: 1 / span 2; + + @include mq('md') { + grid-column: 1 / span 5; + } + + @include mq('lg') { + grid-column: 1 / span 9; + } + } + + &__display-text { + font-size: var(--hero-font-size); + font-weight: var(--wght-black); + font-style: italic; + line-height: var(--lh-tight); + } + + &__soundwave { + width: var(--hero-icon-width); + max-width: 6rem; + margin-left: var(--hero-icon-shift); + } +} + +// ------------------------------------------------------ +// SOUNDWAVE ANIMATION +// ------------------------------------------------------ + +.wave-bar { + opacity: 1; + transition: opacity 0.5s ease; +} + +.wave-bar.is-animating { + animation: wave-flicker 2s cubic-bezier(0.4, 0, 0.2, 1) var(--delay, 0s) + forwards; +} + +@keyframes wave-flicker { + 0% { + opacity: 1; + } + + 20% { + opacity: var(--target-opacity, 0.4); + } + + 40% { + opacity: 1; + } + + 60% { + opacity: var(--target-opacity, 0.7); + } + + 100% { + opacity: 1; + } +} + +@media (prefers-reduced-motion: reduce) { + .wave-bar { + animation: none !important; + transition: none !important; + opacity: 1 !important; + } +} diff --git a/src/styles/layout/_menu.scss b/src/styles/layout/_menu.scss new file mode 100644 index 0000000000..4b04ea6583 --- /dev/null +++ b/src/styles/layout/_menu.scss @@ -0,0 +1,265 @@ +@use '../abstracts' as *; +@use '../base' as *; + +.menu { + position: fixed; + z-index: 100; + inset: 0; + height: 100dvh; + background-color: var(--nav-bg); + display: flex; + flex-direction: column; + transform: translateX(-100%); + visibility: hidden; + will-change: transform; // Firefox animation hint + backface-visibility: hidden; // Firefox animation hint + transition: + transform var(--nav-transition), + visibility var(--nav-transition); + + @include mq('md') { + width: 100%; + max-width: #{rem(500)}; + } + + @include mq('lg') { + max-width: #{rem(800)}; + } + + // @media (min-width: 769px) and (max-width: 1024px) and (orientation: portrait) { + // max-width: #{rem(600)}; + // } + + &.is-open { + transform: translateX(0); + visibility: visible; + } + + // ------------------------------------------------- + // GRID MIRRORING + // ------------------------------------------------- + + &__container { + padding-top: clamp(3rem, 30vw, 7rem); + padding-left: var(--page-side-spacing-s); + + @include mq('md') { + padding-top: 9rem; + padding-left: var(--page-side-spacing-md); + } + + @include mq('lg') { + padding-top: clamp(7.5rem, 8vw, 10rem); + padding-left: var(--dynamic-margin); + } + } + + // ------------------------------------------------- + // MENU LINKS CONTAINER + // ------------------------------------------------- + + &__list { + display: flex; + flex-direction: column; + flex-grow: 1; + overflow-y: auto; + overscroll-behavior: contain; + gap: var(--nav-list-gap); + + // --- focus state (fix with the overflow-y auto) --- + + margin-left: -0.5rem; + margin-top: -0.5rem; + padding: 0.5rem 0.5rem var(--nav-list-padding) 0.5rem; + + // --- fading edge --- + + -webkit-mask-image: linear-gradient(to bottom, black 80%, transparent 100%); + mask-image: linear-gradient(to bottom, black 80%, transparent 100%); + + // --- custom scrollbar --- + + &::-webkit-scrollbar { + width: #{rem(6)}; + } + + &::-webkit-scrollbar-thumb { + background: var(--nav-scrollbar-clr); + border-radius: #{rem(10)}; + } + } + + // ------------------------------------------------------ + // ADDITIONAL MENU LINK STYLES (only for menu__list) + // ------------------------------------------------------ + + &__item { + position: relative; + width: fit-content; + transition: + transform var(--nav-link-transition), + color var(--nav-link-transition); + transform-origin: left bottom; + transform: scaleX(1); + + // --- FLOW LINE under MENU LINK --- + + &::after { + content: ''; + opacity: 0.8; + position: absolute; + bottom: #{rem(-2)}; + left: 0; + width: 100%; + height: 1px; + background-color: var(--nav-link-hover); + transform: scaleX(0); + transform-origin: right; + transition: transform var(--nav-link-transition); + } + + // --- (hover) FLOW LINE under MENU LINK --- + + &:hover { + &::after { + transform: scaleX(1); + transform-origin: left; + } + + .menu-link { + color: var(--nav-link-hover); + } + } + } + + // ------------------------------------------------- + // MAIN MENU LINK STYLES + // ------------------------------------------------- + + &__link { + @extend %link-base; + + display: inline-block; + width: fit-content; + text-transform: uppercase; + color: var(--nav-link-color); + font-size: var(--nav-link-font-size); + font-weight: var(--nav-link-weight); + letter-spacing: var(--nav-spacing); + transition: + color var(--nav-link-transition), + transform var(--nav-link-transition); + + // --- RENDERING FIX --- + + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + backface-visibility: hidden; + transform: translateZ(0); + + &:hover { + color: var(--nav-link-hover); + } + } + + // ------------------------------------------------- + // CONTACT CONTAINER + // ------------------------------------------------- + + &__contact { + transform-origin: left bottom; + display: inline-block; + } + + // --- PHONE NR STYLES --- + + &__tel { + display: flex; + flex-direction: column; + gap: 1rem; + + &-number { + display: inline-block; + font-weight: var(--nav-link-weight); + font-size: var(--fs-s); + } + + // --- CTA --- + + &-label { + display: inline-block; + align-self: flex-start; + font-size: var(--fs-s); + position: relative; + transition: transform var(--nav-link-transition); + + // --- FLOW LINE under CONTACT --- + + &::after { + content: ''; + opacity: 0.8; + position: absolute; + bottom: #{rem(-3)}; + left: 0; + width: 100%; + height: 1px; + background-color: var(--txt-primary); + transform: scaleX(1); + transform-origin: right; + transition: + transform var(--nav-link-transition), + scale var(--nav-link-transition); + } + } + } + + &__contact:hover { + .menu__tel-label::after { + transform: scaleX(0); + } + } + + // ------------------------------------------------- + // BLUR & GRADIENT COVER (menu is open) + // ------------------------------------------------- + + &-overlay { + position: fixed; + inset: 0; + z-index: 99; + opacity: 0; + visibility: hidden; + background: var(--nav-overlay); + transition: + opacity var(--nav-transition), + visibility var(--nav-transition); + pointer-events: none; + + .menu-is-open & { + opacity: 1; + visibility: visible; + pointer-events: auto; + + @include mq('md') { + background: var(--nav-overlay-fallback); + + @supports (backdrop-filter: blur(8px)) { + background: var(--nav-overlay-md); + backdrop-filter: blur(8px); + } + } + + @include mq('lg') { + @supports (backdrop-filter: blur(8px)) { + background: var(--nav-overlay); + backdrop-filter: blur(8px); + } + } + } + } +} + +html.scroll-locked, +body.scroll-locked { + overflow: hidden; +} diff --git a/src/styles/layout/_products.scss b/src/styles/layout/_products.scss new file mode 100644 index 0000000000..774b363a0f --- /dev/null +++ b/src/styles/layout/_products.scss @@ -0,0 +1,29 @@ +@use '../abstracts' as *; +@use '../base' as *; + +.products { + background: var(--bg-section); + min-height: 30vh; + position: relative; + + &__grid { + grid-column: 1 / -1; + display: grid; + gap: var(--gutter); + grid-template-columns: 1fr; + + @include mq('xl') { + grid-template-columns: repeat(3, 1fr); + gap: calc(var(--gutter) * 2); + } + + @include mq('xxl') { + grid-column: 2 / -2; + } + + @media (min-width: 1024px) and (orientation: portrait) { + grid-column: 1 / -1; + gap: var(--gutter); + } + } +} diff --git a/src/styles/layout/_why-us.scss b/src/styles/layout/_why-us.scss new file mode 100644 index 0000000000..dcadb6c6d7 --- /dev/null +++ b/src/styles/layout/_why-us.scss @@ -0,0 +1,75 @@ +@use '../abstracts' as *; + +.why-bose { + &__title { + display: flex; + flex-direction: column; + grid-column: 1 / -1; + padding-bottom: calc(var(--page-title-padding) - var(--gutter)); + text-align: center; + line-height: var(--lh-section-title); + margin: 0 auto; + width: 100%; + + @include mq('md') { + grid-column: 1 / span 3; + padding-bottom: var(--page-title-padding); + margin-bottom: 0; + text-align: left; + + // padding-left: var(--padding-inline); + } + + @include mq('lg') { + grid-column: 1 / span 5; + } + + @include mq('xl') { + grid-column: 1 / span 4; + } + + @include mq('xxl') { + grid-column: 2 / span 4; + } + + @media (min-width: 1024px) and (max-width: 1024px) and (orientation: portrait) { + grid-column: 1 / span 5; + } + } + + &__content { + grid-column: 1 / -1; + display: flex; + flex-direction: column; + padding: 0 var(--padding-inline); + + @include mq('md') { + margin-top: 0.15em; + grid-column: 4 / -1; + gap: 2rem; + padding: 0; + } + + @include mq('lg') { + grid-column: 6 / span 6; + gap: 2rem; + } + + @media (min-width: 1024px) and (max-width: 1024px) and (orientation: portrait) { + grid-column: 6 / span 6; + } + } + + &__list { + margin-bottom: 2rem; + line-height: 2.4; + + @include mq('md') { + margin-bottom: 0.3rem; + } + + @include mq('lg') { + margin-bottom: 0.5rem; + } + } +} diff --git a/src/styles/layout/index.scss b/src/styles/layout/index.scss new file mode 100644 index 0000000000..a118d35817 --- /dev/null +++ b/src/styles/layout/index.scss @@ -0,0 +1,8 @@ +@forward 'header'; +@forward 'menu'; +@forward 'hero'; +@forward 'products'; +@forward 'gallery'; +@forward 'why-us'; +@forward 'contact'; +@forward 'footer'; diff --git a/src/styles/main.scss b/src/styles/main.scss index fb9195d128..b292199644 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -1,7 +1,4 @@ -@import 'utils'; -@import 'fonts'; -@import 'typography'; - -body { - background: $c-gray; -} +@use 'abstracts' as *; +@use 'base' as *; +@use 'components' as *; +@use 'layout' as *; diff --git a/src/styles/utils/_extends.scss b/src/styles/utils/_extends.scss deleted file mode 100644 index d7201e7b3e..0000000000 --- a/src/styles/utils/_extends.scss +++ /dev/null @@ -1,4 +0,0 @@ -%h1 { - font-family: Roboto, sans-serif; - font-weight: 400; -} diff --git a/src/styles/utils/_mixins.scss b/src/styles/utils/_mixins.scss deleted file mode 100644 index 80c79780dc..0000000000 --- a/src/styles/utils/_mixins.scss +++ /dev/null @@ -1,6 +0,0 @@ -@mixin hover($_property, $_toValue) { - transition: #{$_property} 0.3s; - &:hover { - #{$_property}: $_toValue; - } -} diff --git a/src/styles/utils/_vars.scss b/src/styles/utils/_vars.scss deleted file mode 100644 index aeb006ffbb..0000000000 --- a/src/styles/utils/_vars.scss +++ /dev/null @@ -1 +0,0 @@ -$c-gray: #eee;