├── preview.jpg ├── design ├── focus-state.jpg ├── hover-state.jpg ├── loading-state.jpg ├── api-error-state.jpg ├── dropdown-state.jpg ├── no-results-state.jpg ├── desktop-design-metric.jpg ├── mobile-design-metric.jpg ├── desktop-design-imperial.jpg ├── mobile-design-imperial.jpg └── search-in-progress-state.jpg ├── assets ├── images │ ├── icon-fog.webp │ ├── icon-rain.webp │ ├── icon-snow.webp │ ├── icon-storm.webp │ ├── icon-sunny.webp │ ├── favicon-32x32.png │ ├── icon-drizzle.webp │ ├── icon-overcast.webp │ ├── icon-partly-cloudy.webp │ ├── icon-checkmark.svg │ ├── icon-dropdown.svg │ ├── icon-error.svg │ ├── icon-search.svg │ ├── icon-retry.svg │ ├── icon-loading.svg │ ├── icon-units.svg │ ├── bg-today-large.svg │ ├── bg-today-small.svg │ └── logo.svg └── fonts │ ├── DM_Sans │ ├── static │ │ ├── DMSans-Bold.ttf │ │ ├── DMSans-Light.ttf │ │ ├── DMSans-Medium.ttf │ │ ├── DMSans-SemiBold.ttf │ │ └── DMSans-SemiBoldItalic.ttf │ ├── DMSans-VariableFont_opsz,wght.ttf │ ├── DMSans-Italic-VariableFont_opsz,wght.ttf │ ├── README.txt │ └── OFL.txt │ └── Bricolage_Grotesque │ ├── static │ └── BricolageGrotesque-Bold.ttf │ ├── BricolageGrotesque-VariableFont_opsz,wdth,wght.ttf │ ├── README.txt │ └── OFL.txt ├── .gitignore ├── style-guide.md ├── README.md ├── index.html ├── script.js └── styles.css /preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/preview.jpg -------------------------------------------------------------------------------- /design/focus-state.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/design/focus-state.jpg -------------------------------------------------------------------------------- /design/hover-state.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/design/hover-state.jpg -------------------------------------------------------------------------------- /design/loading-state.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/design/loading-state.jpg -------------------------------------------------------------------------------- /assets/images/icon-fog.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/assets/images/icon-fog.webp -------------------------------------------------------------------------------- /design/api-error-state.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/design/api-error-state.jpg -------------------------------------------------------------------------------- /design/dropdown-state.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/design/dropdown-state.jpg -------------------------------------------------------------------------------- /design/no-results-state.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/design/no-results-state.jpg -------------------------------------------------------------------------------- /assets/images/icon-rain.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/assets/images/icon-rain.webp -------------------------------------------------------------------------------- /assets/images/icon-snow.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/assets/images/icon-snow.webp -------------------------------------------------------------------------------- /assets/images/icon-storm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/assets/images/icon-storm.webp -------------------------------------------------------------------------------- /assets/images/icon-sunny.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/assets/images/icon-sunny.webp -------------------------------------------------------------------------------- /assets/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/assets/images/favicon-32x32.png -------------------------------------------------------------------------------- /assets/images/icon-drizzle.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/assets/images/icon-drizzle.webp -------------------------------------------------------------------------------- /assets/images/icon-overcast.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/assets/images/icon-overcast.webp -------------------------------------------------------------------------------- /design/desktop-design-metric.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/design/desktop-design-metric.jpg -------------------------------------------------------------------------------- /design/mobile-design-metric.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/design/mobile-design-metric.jpg -------------------------------------------------------------------------------- /design/desktop-design-imperial.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/design/desktop-design-imperial.jpg -------------------------------------------------------------------------------- /design/mobile-design-imperial.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/design/mobile-design-imperial.jpg -------------------------------------------------------------------------------- /assets/images/icon-partly-cloudy.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/assets/images/icon-partly-cloudy.webp -------------------------------------------------------------------------------- /design/search-in-progress-state.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/design/search-in-progress-state.jpg -------------------------------------------------------------------------------- /assets/fonts/DM_Sans/static/DMSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/assets/fonts/DM_Sans/static/DMSans-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/DM_Sans/static/DMSans-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/assets/fonts/DM_Sans/static/DMSans-Light.ttf -------------------------------------------------------------------------------- /assets/fonts/DM_Sans/static/DMSans-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/assets/fonts/DM_Sans/static/DMSans-Medium.ttf -------------------------------------------------------------------------------- /assets/fonts/DM_Sans/static/DMSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/assets/fonts/DM_Sans/static/DMSans-SemiBold.ttf -------------------------------------------------------------------------------- /assets/fonts/DM_Sans/DMSans-VariableFont_opsz,wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/assets/fonts/DM_Sans/DMSans-VariableFont_opsz,wght.ttf -------------------------------------------------------------------------------- /assets/fonts/DM_Sans/static/DMSans-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/assets/fonts/DM_Sans/static/DMSans-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /assets/fonts/DM_Sans/DMSans-Italic-VariableFont_opsz,wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/assets/fonts/DM_Sans/DMSans-Italic-VariableFont_opsz,wght.ttf -------------------------------------------------------------------------------- /assets/fonts/Bricolage_Grotesque/static/BricolageGrotesque-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/assets/fonts/Bricolage_Grotesque/static/BricolageGrotesque-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/Bricolage_Grotesque/BricolageGrotesque-VariableFont_opsz,wdth,wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ayokanmi-Adejola/Weather-App/HEAD/assets/fonts/Bricolage_Grotesque/BricolageGrotesque-VariableFont_opsz,wdth,wght.ttf -------------------------------------------------------------------------------- /assets/images/icon-checkmark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icon-dropdown.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icon-error.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icon-search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Design files (please do not remove 🙂) 2 | *.sketch 3 | *.fig 4 | *.xd 5 | 6 | # Dependencies 7 | /node_modules 8 | /.pnp 9 | .pnp.js 10 | yarn.debug.log* 11 | yarn.error.log* 12 | npm-debug.log* 13 | 14 | # Environment and secrets 15 | .env 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | # Testing 22 | /coverage 23 | 24 | # Production 25 | /build 26 | /dist 27 | /.next 28 | /out 29 | 30 | # IDEs and editors 31 | /.idea 32 | /.vscode 33 | *.swp 34 | *.swo 35 | 36 | # Misc 37 | *.log 38 | *.pem 39 | .DS_Store 40 | Thumbs.db -------------------------------------------------------------------------------- /assets/images/icon-retry.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icon-loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /style-guide.md: -------------------------------------------------------------------------------- 1 | # Front-end Style Guide 2 | 3 | ## Layout 4 | 5 | The designs were created to the following widths: 6 | 7 | - Mobile: 375px 8 | - Desktop: 1440px 9 | 10 | > 💡 These are just the design sizes. Ensure content is responsive and meets WCAG requirements by testing the full range of screen sizes from 320px to large screens. 11 | 12 | ## Colors 13 | 14 | ### Neutral 15 | 16 | - **Neutral 900**: hsl(243, 96%, 9%) 17 | - **Neutral 800**: hsl(243, 27%, 20%) 18 | - **Neutral 700**: hsl(243, 23%, 24%) 19 | - **Neutral 600**: hsl(243, 23%, 30%) 20 | - **Neutral 300**: hsl(240, 6%, 70%) 21 | - **Neutral 200**: hsl(250, 6%, 84%) 22 | - **Neutral 0**: hsl(0, 0%, 100%) 23 | 24 | ### Orange 25 | 26 | - **Orange 500**: hsl(28, 100%, 52%) 27 | 28 | ### Blue 29 | 30 | - **Blue 500**: hsl(233, 67%, 56%) 31 | - **Blue 700**: hsl(248, 70%, 36%) 32 | 33 | ## Typography 34 | 35 | ### Body Copy 36 | 37 | - Font size: 18px 38 | 39 | ### Font 40 | 41 | - Family: [DM Sans](https://fonts.google.com/specimen/DM+Sans) 42 | - Weights: 300, 500, 600, 600i, 700 43 | 44 | - Family: [Bricolage Grotesque](https://fonts.google.com/specimen/Bricolage+Grotesque) 45 | - Weights: 700 46 | 47 | > 💎 [Upgrade to Pro](https://www.frontendmentor.io/pro?ref=style-guide) for design file access to see all design details and get hands-on experience using a professional workflow with tools like Figma. The design file for this challenge also includes a design system and tablet layout to help you build a more accurate solution faster. -------------------------------------------------------------------------------- /assets/images/icon-units.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/fonts/Bricolage_Grotesque/README.txt: -------------------------------------------------------------------------------- 1 | Bricolage Grotesque Variable Font 2 | ================================= 3 | 4 | This download contains Bricolage Grotesque as both a variable font and static fonts. 5 | 6 | Bricolage Grotesque is a variable font with these axes: 7 | opsz 8 | wdth 9 | wght 10 | 11 | This means all the styles are contained in a single file: 12 | Bricolage_Grotesque/BricolageGrotesque-VariableFont_opsz,wdth,wght.ttf 13 | 14 | If your app fully supports variable fonts, you can now pick intermediate styles 15 | that aren’t available as static fonts. Not all apps support variable fonts, and 16 | in those cases you can use the static font files for Bricolage Grotesque: 17 | 18 | Bricolage_Grotesque/static/BricolageGrotesque-Bold.ttf 19 | 20 | Get started 21 | ----------- 22 | 23 | 1. Install the font files you want to use 24 | 25 | 2. Use your app's font picker to view the font family and all the 26 | available styles 27 | 28 | Learn more about variable fonts 29 | ------------------------------- 30 | 31 | https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts 32 | https://variablefonts.typenetwork.com 33 | https://medium.com/variable-fonts 34 | 35 | In desktop apps 36 | 37 | https://theblog.adobe.com/can-variable-fonts-illustrator-cc 38 | https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts 39 | 40 | Online 41 | 42 | https://developers.google.com/fonts/docs/getting_started 43 | https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide 44 | https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts 45 | 46 | Installing fonts 47 | 48 | MacOS: https://support.apple.com/en-us/HT201749 49 | Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux 50 | Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows 51 | 52 | Android Apps 53 | 54 | https://developers.google.com/fonts/docs/android 55 | https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts 56 | 57 | License 58 | ------- 59 | Please read the full license text (OFL.txt) to understand the permissions, 60 | restrictions and requirements for usage, redistribution, and modification. 61 | 62 | You can use them in your products & projects – print or digital, 63 | commercial or otherwise. 64 | 65 | This isn't legal advice, please consider consulting a lawyer and see the full 66 | license for all details. 67 | -------------------------------------------------------------------------------- /assets/fonts/DM_Sans/README.txt: -------------------------------------------------------------------------------- 1 | DM Sans Variable Font 2 | ===================== 3 | 4 | This download contains DM Sans as both variable fonts and static fonts. 5 | 6 | DM Sans is a variable font with these axes: 7 | opsz 8 | wght 9 | 10 | This means all the styles are contained in these files: 11 | DM_Sans/DMSans-VariableFont_opsz,wght.ttf 12 | DM_Sans/DMSans-Italic-VariableFont_opsz,wght.ttf 13 | 14 | If your app fully supports variable fonts, you can now pick intermediate styles 15 | that aren’t available as static fonts. Not all apps support variable fonts, and 16 | in those cases you can use the static font files for DM Sans: 17 | 18 | DM_Sans/static/DMSans-Light.ttf 19 | DM_Sans/static/DMSans-Medium.ttf 20 | DM_Sans/static/DMSans-SemiBold.ttf 21 | DM_Sans/static/DMSans-Bold.ttf 22 | DM_Sans/static/DMSans-SemiBoldItalic.ttf 23 | 24 | Get started 25 | ----------- 26 | 27 | 1. Install the font files you want to use 28 | 29 | 2. Use your app's font picker to view the font family and all the 30 | available styles 31 | 32 | Learn more about variable fonts 33 | ------------------------------- 34 | 35 | https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts 36 | https://variablefonts.typenetwork.com 37 | https://medium.com/variable-fonts 38 | 39 | In desktop apps 40 | 41 | https://theblog.adobe.com/can-variable-fonts-illustrator-cc 42 | https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts 43 | 44 | Online 45 | 46 | https://developers.google.com/fonts/docs/getting_started 47 | https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide 48 | https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts 49 | 50 | Installing fonts 51 | 52 | MacOS: https://support.apple.com/en-us/HT201749 53 | Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux 54 | Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows 55 | 56 | Android Apps 57 | 58 | https://developers.google.com/fonts/docs/android 59 | https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts 60 | 61 | License 62 | ------- 63 | Please read the full license text (OFL.txt) to understand the permissions, 64 | restrictions and requirements for usage, redistribution, and modification. 65 | 66 | You can use them in your products & projects – print or digital, 67 | commercial or otherwise. 68 | 69 | This isn't legal advice, please consider consulting a lawyer and see the full 70 | license for all details. 71 | -------------------------------------------------------------------------------- /assets/fonts/DM_Sans/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2014 The DM Sans Project Authors (https://github.com/googlefonts/dm-fonts) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | https://openfontlicense.org 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /assets/fonts/Bricolage_Grotesque/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2022 The Bricolage Grotesque Project Authors (https://github.com/ateliertriay/bricolage) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | https://openfontlicense.org 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /assets/images/bg-today-large.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/bg-today-small.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Weather Application 2 | 3 | ![Design preview for the Weather app coding challenge](./preview.jpg) 4 | 5 | A comprehensive, responsive weather application built with vanilla HTML, CSS, and JavaScript. Features real-time weather data, location search, favorites system, and delightful animations. 6 | 7 | ## 🌟 Features Implemented 8 | 9 | ### Core Requirements ✅ 10 | - **Location Search**: Search for weather information by entering any location 11 | - **Current Weather**: Temperature, weather icon, and location details 12 | - **Weather Metrics**: "Feels like" temperature, humidity, wind speed, precipitation 13 | - **7-Day Forecast**: Daily high/low temperatures with weather icons 14 | - **Hourly Forecast**: Temperature changes throughout the day with day selector 15 | - **Units System**: Toggle between Imperial/Metric units with granular control 16 | - **Responsive Design**: Optimal layout for mobile, tablet, and desktop 17 | - **Interactive States**: Hover and focus states for all interactive elements 18 | 19 | ### Enhanced Features 🚀 20 | - **Geolocation Support**: Automatic current location detection 21 | - **Favorites System**: Save and quickly access frequently checked locations 22 | - **Dark/Light Mode**: Automatic theme switching based on time of day 23 | - **Weather Animations**: Dynamic backgrounds and particles based on conditions 24 | - **Additional Data**: UV index, visibility, air pressure, sunrise/sunset times 25 | - **Accessibility**: Full keyboard navigation, screen reader support, reduced motion 26 | - **Performance**: Debounced search, efficient DOM updates, optimized animations 27 | 28 | | Desktop view | Mobile view | 29 | | ------- | ------ | 30 | | ![Desktop](https://github.com/Ayokanmi-Adejola/Weather-App/blob/main/design/desktop-design-imperial.jpg?raw=true) | ![Mobile](https://github.com/Ayokanmi-Adejola/Weather-App/blob/main/design/mobile-design-imperial.jpg?raw=true) | 31 | 32 | ## 🚀 Getting Started 33 | 34 | 1. Clone or download the project files ```git clone https://github.com/Ayokanmi-Adejola/Weather-App``` 35 | 2. Open `index.html` in a modern web browser 36 | 3. Allow location access for automatic weather detection (optional) 37 | 4. Start exploring weather data for any location! 38 | 39 | ## 🛠️ Technical Implementation 40 | 41 | ### APIs Used 42 | - **Open-Meteo Weather API**: Free weather data with no API key required 43 | - **Open-Meteo Geocoding API**: Location search and coordinates 44 | 45 | ### Browser Support 46 | - Chrome 90+ | Firefox 88+ | Safari 14+ | Edge 90+ 47 | 48 | ### Architecture 49 | - **Vanilla JavaScript**: No frameworks, pure ES6+ features 50 | - **CSS Custom Properties**: Consistent design system 51 | - **Semantic HTML**: Accessible and SEO-friendly structure 52 | - **Progressive Enhancement**: Works without JavaScript for basic content 53 | 54 | ## 📱 Responsive Design 55 | 56 | - **Mobile**: 320px - 768px (Touch-optimized interface) 57 | - **Tablet**: 768px - 1024px (Hybrid layout) 58 | - **Desktop**: 1024px+ (Full feature set) 59 | 60 | ## ♿ Accessibility Features 61 | 62 | - **Keyboard Navigation**: Full keyboard support for all interactions 63 | - **Screen Reader Support**: Proper ARIA labels and semantic HTML 64 | - **High Contrast Mode**: Enhanced visibility for visual impairments 65 | - **Reduced Motion**: Respects user's motion preferences 66 | - **Skip Links**: Quick navigation for assistive technologies 67 | 68 | ## 🎨 Design System 69 | 70 | ### Colors 71 | - **Neutral Palette**: 9 shades from `hsl(243, 96%, 9%)` to `hsl(0, 0%, 100%)` 72 | - **Accent Colors**: Blue `hsl(233, 67%, 56%)` and Orange `hsl(28, 100%, 52%)` 73 | - **Weather Themes**: Dynamic backgrounds based on weather conditions 74 | 75 | ### Typography 76 | - **Primary Font**: DM Sans (300, 500, 600, 700) 77 | - **Display Font**: Bricolage Grotesque (700) 78 | - **Base Size**: 18px with responsive scaling 79 | 80 | ### Spacing & Layout 81 | - **Consistent Scale**: 0.5rem to 4rem using CSS custom properties 82 | - **Grid System**: CSS Grid for responsive layouts 83 | - **Component-based**: Reusable card and button components 84 | 85 | ## 🌦️ Weather Data 86 | 87 | ### Current Conditions 88 | - Temperature and "feels like" temperature 89 | - Weather description with animated icon 90 | - Humidity, wind speed, precipitation 91 | - UV index, visibility, air pressure 92 | - Sunrise and sunset times 93 | 94 | ### Forecasts 95 | - **Daily**: 7-day forecast with high/low temperatures 96 | - **Hourly**: 24-hour detailed forecast with day selector 97 | 98 | ## 🔧 Customization 99 | 100 | ### Adding Weather Icons 101 | 1. Add icon files to `assets/images/` 102 | 2. Update `WEATHER_CODES` object in `script.js` 103 | 3. Map weather codes to new icons 104 | 105 | ### Modifying Themes 106 | 1. Update CSS custom properties in `:root` and `[data-theme]` selectors 107 | 2. Add theme variants in theme management system 108 | 109 | ### Extending API Data 110 | 1. Modify API parameters in `getWeatherData` function 111 | 2. Update display functions for new data 112 | 3. Add corresponding HTML elements and CSS styles 113 | 114 | ## 🚀 Performance Features 115 | 116 | - **Debounced Search**: Reduces API calls during typing 117 | - **Efficient DOM Updates**: Minimal reflows and repaints 118 | - **Optimized Animations**: Uses `requestAnimationFrame` and CSS transforms 119 | - **Lazy Loading**: Weather particles created on demand 120 | - **Local Storage**: Caches user preferences and favorites 121 | 122 | ## 🐛 Known Issues & Limitations 123 | 124 | - Weather particles may impact performance on older devices 125 | - Some weather data may not be available for all locations 126 | - Geolocation requires HTTPS in production environments 127 | - API rate limits may apply for excessive usage 128 | 129 | ## 🚀 Future Enhancements 130 | 131 | - Weather alerts and notifications 132 | - Historical weather data visualization 133 | - Interactive weather maps 134 | - Social sharing capabilities 135 | - Offline support with service workers 136 | - Weather widgets for embedding 137 | - Multi-language support 138 | - Weather-based recommendations 139 | 140 | ## 📄 Project Structure 141 | 142 | ``` 143 | weather-app-main/ 144 | ├── index.html # Main HTML file 145 | ├── styles.css # Complete CSS with design system 146 | ├── script.js # JavaScript functionality 147 | ├── assets/ 148 | │ └── images/ # Weather icons and UI assets 149 | ├── style-guide.md # Design specifications 150 | └── README.md # This file 151 | ``` 152 | 153 | ## 🙏 Acknowledgments 154 | 155 | - **Frontend Mentor**: Original challenge and design inspiration 156 | - **Open-Meteo**: Free weather API with comprehensive data 157 | - **Google Fonts**: Typography (DM Sans, Bricolage Grotesque) 158 | - **Community**: Feedback and support during development 159 | 160 | ## 📞 Support & Troubleshooting 161 | 162 | ### Common Issues 163 | 1. **Location not found**: Try different search terms or check spelling 164 | 2. **Weather data not loading**: Check internet connection and browser console 165 | 3. **Geolocation not working**: Ensure HTTPS and location permissions 166 | 4. **Performance issues**: Disable animations in accessibility settings 167 | 168 | ### Browser Console 169 | Check the browser developer tools console for detailed error messages and debugging information. 170 | 171 | 172 | ## 🌟 Frontend Mentor Challenge 173 | 174 | This project was built as a solution to the [Frontend Mentor Weather App Challenge](https://www.frontendmentor.io). 175 | 176 | **Challenge completed with enhanced features beyond requirements:** 177 | - ✅ All original requirements implemented 178 | - 🚀 Additional features: Geolocation, Favorites, Themes, Animations 179 | - ♿ Enhanced accessibility and performance 180 | - 📱 Improved responsive design 181 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Weather Now 17 | 18 | 19 |
20 | 21 |
22 | 23 |
24 | 25 |
26 | 29 | 30 |
31 | 32 | 35 | 36 | 37 |
38 | 43 | 44 | 55 |
56 | 57 | 58 |
59 | 64 | 65 | 122 |
123 |
124 | 125 | 126 |
127 | 128 |

How's the sky looking today?

129 | 130 | 131 |
132 |
133 |
134 | 135 | 143 |
144 | 147 | 148 | 149 |
150 | 151 |
152 |
153 |
154 | 155 | 156 |
157 | Loading... 158 |

Loading weather data...

159 |
160 | 161 | 162 | 171 | 172 | 173 |
426 |
427 |
428 | 429 | 430 | 431 | 432 | 433 | 434 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | // Weather App JavaScript 2 | 3 | // API Configuration 4 | const API_CONFIG = { 5 | geocoding: 'https://geocoding-api.open-meteo.com/v1/search', 6 | weather: 'https://api.open-meteo.com/v1/forecast' 7 | }; 8 | 9 | // Weather code mappings for icons 10 | const WEATHER_CODES = { 11 | 0: { description: 'Clear sky', icon: 'sunny' }, 12 | 1: { description: 'Mainly clear', icon: 'sunny' }, 13 | 2: { description: 'Partly cloudy', icon: 'partly-cloudy' }, 14 | 3: { description: 'Overcast', icon: 'overcast' }, 15 | 45: { description: 'Fog', icon: 'fog' }, 16 | 48: { description: 'Depositing rime fog', icon: 'fog' }, 17 | 51: { description: 'Light drizzle', icon: 'drizzle' }, 18 | 53: { description: 'Moderate drizzle', icon: 'drizzle' }, 19 | 55: { description: 'Dense drizzle', icon: 'drizzle' }, 20 | 56: { description: 'Light freezing drizzle', icon: 'drizzle' }, 21 | 57: { description: 'Dense freezing drizzle', icon: 'drizzle' }, 22 | 61: { description: 'Slight rain', icon: 'rain' }, 23 | 63: { description: 'Moderate rain', icon: 'rain' }, 24 | 65: { description: 'Heavy rain', icon: 'rain' }, 25 | 66: { description: 'Light freezing rain', icon: 'rain' }, 26 | 67: { description: 'Heavy freezing rain', icon: 'rain' }, 27 | 71: { description: 'Slight snow fall', icon: 'snow' }, 28 | 73: { description: 'Moderate snow fall', icon: 'snow' }, 29 | 75: { description: 'Heavy snow fall', icon: 'snow' }, 30 | 77: { description: 'Snow grains', icon: 'snow' }, 31 | 80: { description: 'Slight rain showers', icon: 'rain' }, 32 | 81: { description: 'Moderate rain showers', icon: 'rain' }, 33 | 82: { description: 'Violent rain showers', icon: 'rain' }, 34 | 85: { description: 'Slight snow showers', icon: 'snow' }, 35 | 86: { description: 'Heavy snow showers', icon: 'snow' }, 36 | 95: { description: 'Thunderstorm', icon: 'storm' }, 37 | 96: { description: 'Thunderstorm with slight hail', icon: 'storm' }, 38 | 99: { description: 'Thunderstorm with heavy hail', icon: 'storm' } 39 | }; 40 | 41 | // Application State 42 | const appState = { 43 | currentLocation: null, 44 | weatherData: null, 45 | units: { 46 | temperature: 'celsius', 47 | windSpeed: 'kmh', 48 | precipitation: 'mm' 49 | }, 50 | theme: 'dark', 51 | favorites: JSON.parse(localStorage.getItem('weatherAppFavorites') || '[]'), 52 | selectedDay: 0 // For hourly forecast day selection 53 | }; 54 | 55 | // DOM Elements (will be populated when DOM is loaded) 56 | const elements = {}; 57 | 58 | // Utility Functions 59 | const utils = { 60 | // Format temperature based on current units 61 | formatTemperature(temp) { 62 | if (appState.units.temperature === 'fahrenheit') { 63 | return `${Math.round(temp * 9/5 + 32)}°F`; 64 | } 65 | return `${Math.round(temp)}°C`; 66 | }, 67 | 68 | // Format wind speed based on current units 69 | formatWindSpeed(speed) { 70 | if (appState.units.windSpeed === 'mph') { 71 | return `${Math.round(speed * 0.621371)} mph`; 72 | } 73 | return `${Math.round(speed)} km/h`; 74 | }, 75 | 76 | // Format precipitation based on current units 77 | formatPrecipitation(amount) { 78 | if (appState.units.precipitation === 'inches') { 79 | return `${(amount * 0.0393701).toFixed(1)} in`; 80 | } 81 | return `${amount.toFixed(1)} mm`; 82 | }, 83 | 84 | // Get weather icon path 85 | getWeatherIcon(weatherCode) { 86 | const weather = WEATHER_CODES[weatherCode] || WEATHER_CODES[0]; 87 | return `./assets/images/icon-${weather.icon}.webp`; 88 | }, 89 | 90 | // Get weather description 91 | getWeatherDescription(weatherCode) { 92 | const weather = WEATHER_CODES[weatherCode] || WEATHER_CODES[0]; 93 | return weather.description; 94 | }, 95 | 96 | // Format date 97 | formatDate(dateString) { 98 | const date = new Date(dateString); 99 | return date.toLocaleDateString('en-US', { 100 | weekday: 'short', 101 | month: 'short', 102 | day: 'numeric' 103 | }); 104 | }, 105 | 106 | // Format time 107 | formatTime(dateString) { 108 | const date = new Date(dateString); 109 | return date.toLocaleTimeString('en-US', { 110 | hour: 'numeric', 111 | hour12: true 112 | }); 113 | }, 114 | 115 | // Debounce function for search 116 | debounce(func, wait) { 117 | let timeout; 118 | return function executedFunction(...args) { 119 | const later = () => { 120 | clearTimeout(timeout); 121 | func(...args); 122 | }; 123 | clearTimeout(timeout); 124 | timeout = setTimeout(later, wait); 125 | }; 126 | }, 127 | 128 | // Show loading state 129 | showLoading(element) { 130 | element.classList.add('loading'); 131 | }, 132 | 133 | // Hide loading state 134 | hideLoading(element) { 135 | element.classList.remove('loading'); 136 | }, 137 | 138 | // Show error message 139 | showError(message) { 140 | // TODO: Implement error display 141 | console.error(message); 142 | } 143 | }; 144 | 145 | // API Functions 146 | const api = { 147 | // Search for locations 148 | async searchLocations(query) { 149 | try { 150 | const response = await fetch( 151 | `${API_CONFIG.geocoding}?name=${encodeURIComponent(query)}&count=5&language=en&format=json` 152 | ); 153 | 154 | if (!response.ok) { 155 | throw new Error('Failed to search locations'); 156 | } 157 | 158 | const data = await response.json(); 159 | return data.results || []; 160 | } catch (error) { 161 | utils.showError('Failed to search locations'); 162 | return []; 163 | } 164 | }, 165 | 166 | // Get weather data 167 | async getWeatherData(latitude, longitude, retryCount = 0) { 168 | console.log('API: Fetching weather data for:', latitude, longitude, 'Retry:', retryCount); 169 | try { 170 | const params = new URLSearchParams({ 171 | latitude: latitude, 172 | longitude: longitude, 173 | hourly: [ 174 | 'temperature_2m', 175 | 'relative_humidity_2m', 176 | 'apparent_temperature', 177 | 'precipitation_probability', 178 | 'precipitation', 179 | 'weather_code', 180 | 'surface_pressure', 181 | 'wind_speed_10m', 182 | 'wind_direction_10m' 183 | ].join(','), 184 | daily: [ 185 | 'weather_code', 186 | 'temperature_2m_max', 187 | 'temperature_2m_min', 188 | 'sunrise', 189 | 'sunset', 190 | 'uv_index_max', 191 | 'precipitation_sum', 192 | 'wind_speed_10m_max' 193 | ].join(','), 194 | current: [ 195 | 'temperature_2m', 196 | 'relative_humidity_2m', 197 | 'apparent_temperature', 198 | 'precipitation', 199 | 'weather_code', 200 | 'surface_pressure', 201 | 'wind_speed_10m', 202 | 'wind_direction_10m', 203 | 'visibility' 204 | ].join(','), 205 | timezone: 'auto', 206 | forecast_days: 7 207 | }); 208 | 209 | const url = `${API_CONFIG.weather}?${params}`; 210 | console.log('API: Making request to:', url); 211 | 212 | // Add timeout to the fetch request 213 | const controller = new AbortController(); 214 | const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout 215 | 216 | const response = await fetch(url, { 217 | signal: controller.signal, 218 | headers: { 219 | 'Accept': 'application/json', 220 | } 221 | }); 222 | 223 | clearTimeout(timeoutId); 224 | 225 | if (!response.ok) { 226 | console.error('API: Response not ok:', response.status, response.statusText); 227 | throw new Error(`Failed to fetch weather data: ${response.status} ${response.statusText}`); 228 | } 229 | 230 | const data = await response.json(); 231 | console.log('API: Successfully received data'); 232 | return data; 233 | } catch (error) { 234 | console.error('API: Error fetching weather data:', error); 235 | 236 | // Retry up to 2 times with exponential backoff 237 | if (retryCount < 2) { 238 | const delay = Math.pow(2, retryCount) * 1000; // 1s, 2s delays 239 | console.log(`API: Retrying in ${delay}ms...`); 240 | await new Promise(resolve => setTimeout(resolve, delay)); 241 | return this.getWeatherData(latitude, longitude, retryCount + 1); 242 | } 243 | 244 | utils.showError('Failed to fetch weather data'); 245 | return null; 246 | } 247 | } 248 | }; 249 | 250 | // Theme Management 251 | const theme = { 252 | init() { 253 | console.log('Initializing theme system...'); 254 | this.bindEvents(); 255 | 256 | const savedTheme = localStorage.getItem('weatherAppTheme'); 257 | const autoTheme = localStorage.getItem('weatherAppAutoTheme') !== 'false'; 258 | 259 | console.log('Saved theme:', savedTheme); 260 | console.log('Auto theme enabled:', autoTheme); 261 | 262 | if (autoTheme && !savedTheme) { 263 | this.setAutoTheme(); 264 | } else { 265 | const themeToUse = savedTheme || 'dark'; 266 | this.setTheme(themeToUse); 267 | } 268 | 269 | this.updateThemeIcon(); 270 | console.log('Theme system initialized with theme:', appState.theme); 271 | }, 272 | 273 | bindEvents() { 274 | const themeToggle = document.getElementById('themeToggle'); 275 | if (themeToggle) { 276 | themeToggle.addEventListener('click', () => { 277 | this.toggle(); 278 | }); 279 | 280 | // Add keyboard support 281 | themeToggle.addEventListener('keydown', (e) => { 282 | if (e.key === 'Enter' || e.key === ' ') { 283 | e.preventDefault(); 284 | this.toggle(); 285 | } 286 | }); 287 | } 288 | 289 | // Add keyboard shortcut (Ctrl/Cmd + Shift + T) 290 | document.addEventListener('keydown', (e) => { 291 | if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'T') { 292 | e.preventDefault(); 293 | this.toggle(); 294 | } 295 | }); 296 | }, 297 | 298 | setTheme(themeName) { 299 | console.log('Setting theme to:', themeName); 300 | appState.theme = themeName; 301 | document.documentElement.setAttribute('data-theme', themeName); 302 | localStorage.setItem('weatherAppTheme', themeName); 303 | localStorage.setItem('weatherAppAutoTheme', 'false'); 304 | this.updateThemeIcon(); 305 | 306 | // Dispatch custom event for theme change 307 | window.dispatchEvent(new CustomEvent('themeChanged', { 308 | detail: { theme: themeName } 309 | })); 310 | }, 311 | 312 | setAutoTheme() { 313 | const hour = new Date().getHours(); 314 | const isNight = hour < 6 || hour >= 18; 315 | const autoTheme = isNight ? 'dark' : 'light'; 316 | 317 | appState.theme = autoTheme; 318 | document.documentElement.setAttribute('data-theme', autoTheme); 319 | localStorage.setItem('weatherAppAutoTheme', 'true'); 320 | this.updateThemeIcon(); 321 | }, 322 | 323 | toggle() { 324 | const themeToggle = document.getElementById('themeToggle'); 325 | 326 | // Add visual feedback 327 | if (themeToggle) { 328 | themeToggle.style.transform = 'scale(0.95)'; 329 | setTimeout(() => { 330 | themeToggle.style.transform = 'scale(1)'; 331 | }, 150); 332 | } 333 | 334 | const newTheme = appState.theme === 'dark' ? 'light' : 'dark'; 335 | this.setTheme(newTheme); 336 | 337 | // Log theme change for debugging 338 | console.log(`Theme switched to: ${newTheme}`); 339 | }, 340 | 341 | updateThemeIcon() { 342 | const themeIcon = document.getElementById('themeIcon'); 343 | const themeToggle = document.getElementById('themeToggle'); 344 | 345 | if (themeIcon && themeToggle) { 346 | // Add transition class for smooth icon change 347 | themeIcon.style.transition = 'opacity 0.2s ease-in-out'; 348 | 349 | if (appState.theme === 'dark') { 350 | themeIcon.src = './assets/images/icon-sunny.webp'; 351 | themeIcon.alt = 'Switch to light mode'; 352 | themeToggle.setAttribute('aria-label', 'Switch to light mode'); 353 | themeToggle.setAttribute('title', 'Switch to light mode'); 354 | } else { 355 | themeIcon.src = './assets/images/icon-overcast.webp'; 356 | themeIcon.alt = 'Switch to dark mode'; 357 | themeToggle.setAttribute('aria-label', 'Switch to dark mode'); 358 | themeToggle.setAttribute('title', 'Switch to dark mode'); 359 | } 360 | } 361 | } 362 | }; 363 | 364 | // Animations and Visual Effects 365 | const animations = { 366 | init() { 367 | this.app = document.getElementById('app'); 368 | this.particlesContainer = document.getElementById('weatherParticles'); 369 | }, 370 | 371 | setWeatherBackground(weatherCode) { 372 | if (!this.app) return; 373 | 374 | // Remove existing weather classes 375 | this.app.classList.remove('weather-clear', 'weather-cloudy', 'weather-rainy', 'weather-snowy', 'weather-stormy'); 376 | 377 | // Add appropriate weather class based on weather code 378 | if (weatherCode === 0 || weatherCode === 1) { 379 | this.app.classList.add('weather-clear'); 380 | this.createParticles('clear'); 381 | } else if (weatherCode === 2 || weatherCode === 3) { 382 | this.app.classList.add('weather-cloudy'); 383 | this.createParticles('cloudy'); 384 | } else if (weatherCode >= 51 && weatherCode <= 67) { 385 | this.app.classList.add('weather-rainy'); 386 | this.createParticles('rain'); 387 | } else if (weatherCode >= 71 && weatherCode <= 86) { 388 | this.app.classList.add('weather-snowy'); 389 | this.createParticles('snow'); 390 | } else if (weatherCode >= 95) { 391 | this.app.classList.add('weather-stormy'); 392 | this.createParticles('storm'); 393 | } else { 394 | this.app.classList.add('weather-cloudy'); 395 | this.createParticles('cloudy'); 396 | } 397 | }, 398 | 399 | createParticles(weatherType) { 400 | if (!this.particlesContainer) return; 401 | 402 | // Clear existing particles 403 | this.particlesContainer.innerHTML = ''; 404 | 405 | let particleCount = 0; 406 | let particleClass = ''; 407 | 408 | switch (weatherType) { 409 | case 'rain': 410 | particleCount = 50; 411 | particleClass = 'rain'; 412 | break; 413 | case 'snow': 414 | particleCount = 30; 415 | particleClass = 'snow'; 416 | break; 417 | case 'clear': 418 | particleCount = 10; 419 | particleClass = 'clear'; 420 | break; 421 | case 'storm': 422 | particleCount = 60; 423 | particleClass = 'rain'; 424 | break; 425 | default: 426 | particleCount = 5; 427 | particleClass = 'clear'; 428 | } 429 | 430 | for (let i = 0; i < particleCount; i++) { 431 | const particle = document.createElement('div'); 432 | particle.className = `particle ${particleClass}`; 433 | 434 | // Random positioning 435 | particle.style.left = Math.random() * 100 + '%'; 436 | particle.style.animationDelay = Math.random() * 3 + 's'; 437 | particle.style.animationDuration = (Math.random() * 2 + 1) + 's'; 438 | 439 | if (particleClass === 'clear') { 440 | particle.style.width = Math.random() * 4 + 2 + 'px'; 441 | particle.style.height = particle.style.width; 442 | } 443 | 444 | this.particlesContainer.appendChild(particle); 445 | } 446 | }, 447 | 448 | animateValue(element, start, end, duration = 1000) { 449 | const startTime = performance.now(); 450 | const startValue = parseFloat(start) || 0; 451 | const endValue = parseFloat(end) || 0; 452 | const difference = endValue - startValue; 453 | 454 | const animate = (currentTime) => { 455 | const elapsed = currentTime - startTime; 456 | const progress = Math.min(elapsed / duration, 1); 457 | 458 | // Easing function (ease-out) 459 | const easeOut = 1 - Math.pow(1 - progress, 3); 460 | const currentValue = startValue + (difference * easeOut); 461 | 462 | if (element.textContent.includes('°')) { 463 | element.textContent = Math.round(currentValue) + '°'; 464 | } else if (element.textContent.includes('%')) { 465 | element.textContent = Math.round(currentValue) + '%'; 466 | } else { 467 | element.textContent = Math.round(currentValue); 468 | } 469 | 470 | if (progress < 1) { 471 | requestAnimationFrame(animate); 472 | } 473 | }; 474 | 475 | requestAnimationFrame(animate); 476 | } 477 | }; 478 | 479 | // Favorites Management 480 | const favorites = { 481 | init() { 482 | this.bindEvents(); 483 | this.updateUI(); 484 | }, 485 | 486 | bindEvents() { 487 | // Favorite button in current weather 488 | const favoriteButton = document.getElementById('favoriteButton'); 489 | if (favoriteButton) { 490 | favoriteButton.addEventListener('click', () => { 491 | this.toggleCurrentLocation(); 492 | }); 493 | } 494 | 495 | // Favorites dropdown 496 | const favoritesButton = document.getElementById('favoritesButton'); 497 | const favoritesMenu = document.getElementById('favoritesMenu'); 498 | 499 | if (favoritesButton && favoritesMenu) { 500 | favoritesButton.addEventListener('click', () => { 501 | const isOpen = favoritesMenu.classList.contains('active'); 502 | if (isOpen) { 503 | favoritesMenu.classList.remove('active'); 504 | favoritesButton.setAttribute('aria-expanded', 'false'); 505 | } else { 506 | favoritesMenu.classList.add('active'); 507 | favoritesButton.setAttribute('aria-expanded', 'true'); 508 | this.updateFavoritesList(); 509 | } 510 | }); 511 | 512 | // Close favorites menu when clicking outside 513 | document.addEventListener('click', (e) => { 514 | if (!favoritesButton.contains(e.target) && !favoritesMenu.contains(e.target)) { 515 | favoritesMenu.classList.remove('active'); 516 | favoritesButton.setAttribute('aria-expanded', 'false'); 517 | } 518 | }); 519 | } 520 | }, 521 | 522 | toggleCurrentLocation() { 523 | if (!appState.currentLocation) return; 524 | 525 | const favoriteButton = document.getElementById('favoriteButton'); 526 | 527 | if (this.isFavorite(appState.currentLocation)) { 528 | this.remove(appState.currentLocation); 529 | favoriteButton.classList.remove('active'); 530 | favoriteButton.setAttribute('aria-label', 'Add to favorites'); 531 | } else { 532 | this.add(appState.currentLocation); 533 | favoriteButton.classList.add('active'); 534 | favoriteButton.setAttribute('aria-label', 'Remove from favorites'); 535 | } 536 | }, 537 | 538 | add(location) { 539 | const favorite = { 540 | id: Date.now(), 541 | name: location.name, 542 | country: location.country, 543 | latitude: location.latitude, 544 | longitude: location.longitude 545 | }; 546 | 547 | appState.favorites.push(favorite); 548 | this.save(); 549 | this.updateUI(); 550 | }, 551 | 552 | remove(location) { 553 | appState.favorites = appState.favorites.filter(fav => 554 | !(fav.latitude === location.latitude && fav.longitude === location.longitude) 555 | ); 556 | this.save(); 557 | this.updateUI(); 558 | }, 559 | 560 | removeById(id) { 561 | appState.favorites = appState.favorites.filter(fav => fav.id !== id); 562 | this.save(); 563 | this.updateUI(); 564 | }, 565 | 566 | save() { 567 | localStorage.setItem('weatherAppFavorites', JSON.stringify(appState.favorites)); 568 | }, 569 | 570 | isFavorite(location) { 571 | return appState.favorites.some(fav => 572 | fav.latitude === location.latitude && fav.longitude === location.longitude 573 | ); 574 | }, 575 | 576 | updateUI() { 577 | // Update favorite button state 578 | const favoriteButton = document.getElementById('favoriteButton'); 579 | if (favoriteButton && appState.currentLocation) { 580 | if (this.isFavorite(appState.currentLocation)) { 581 | favoriteButton.classList.add('active'); 582 | favoriteButton.setAttribute('aria-label', 'Remove from favorites'); 583 | } else { 584 | favoriteButton.classList.remove('active'); 585 | favoriteButton.setAttribute('aria-label', 'Add to favorites'); 586 | } 587 | } 588 | }, 589 | 590 | updateFavoritesList() { 591 | const favoritesList = document.getElementById('favoritesList'); 592 | const noFavorites = document.getElementById('noFavorites'); 593 | 594 | if (!favoritesList) return; 595 | 596 | if (appState.favorites.length === 0) { 597 | favoritesList.innerHTML = ` 598 |
599 |

No saved locations yet

600 |

Add locations to your favorites for quick access

601 |
602 | `; 603 | } else { 604 | const favoritesHTML = appState.favorites.map(favorite => ` 605 |
606 |
607 |
${favorite.name}
608 |
${favorite.country}
609 |
610 | 613 |
614 | `).join(''); 615 | 616 | favoritesList.innerHTML = favoritesHTML; 617 | 618 | // Add event listeners 619 | favoritesList.querySelectorAll('.favorite-item').forEach(item => { 620 | item.addEventListener('click', (e) => { 621 | if (e.target.classList.contains('favorite-remove')) return; 622 | 623 | const favoriteId = parseInt(item.dataset.favoriteId); 624 | const favorite = appState.favorites.find(fav => fav.id === favoriteId); 625 | 626 | if (favorite) { 627 | appState.currentLocation = favorite; 628 | weather.loadWeatherData(favorite.latitude, favorite.longitude); 629 | 630 | // Close favorites menu 631 | const favoritesMenu = document.getElementById('favoritesMenu'); 632 | const favoritesButton = document.getElementById('favoritesButton'); 633 | if (favoritesMenu && favoritesButton) { 634 | favoritesMenu.classList.remove('active'); 635 | favoritesButton.setAttribute('aria-expanded', 'false'); 636 | } 637 | } 638 | }); 639 | }); 640 | 641 | favoritesList.querySelectorAll('.favorite-remove').forEach(button => { 642 | button.addEventListener('click', (e) => { 643 | e.stopPropagation(); 644 | const favoriteId = parseInt(button.dataset.favoriteId); 645 | this.removeById(favoriteId); 646 | }); 647 | }); 648 | } 649 | } 650 | }; 651 | 652 | // Geolocation 653 | const geolocation = { 654 | async getCurrentPosition() { 655 | return new Promise((resolve, reject) => { 656 | if (!navigator.geolocation) { 657 | reject(new Error('Geolocation is not supported')); 658 | return; 659 | } 660 | 661 | navigator.geolocation.getCurrentPosition( 662 | position => resolve(position), 663 | error => reject(error), 664 | { timeout: 10000, enableHighAccuracy: true } 665 | ); 666 | }); 667 | }, 668 | 669 | async loadCurrentLocationWeather() { 670 | console.log('Attempting to get current location...'); 671 | try { 672 | const position = await this.getCurrentPosition(); 673 | const { latitude, longitude } = position.coords; 674 | console.log('Got current position:', latitude, longitude); 675 | 676 | // Get location name from reverse geocoding (simplified) 677 | appState.currentLocation = { 678 | name: 'Current Location', 679 | latitude, 680 | longitude 681 | }; 682 | 683 | await weather.loadWeatherData(latitude, longitude); 684 | } catch (error) { 685 | console.log('Could not get current location:', error.message); 686 | 687 | // Provide more specific error handling 688 | if (error.code === 1) { 689 | console.log('Geolocation permission denied, using default location'); 690 | } else if (error.code === 2) { 691 | console.log('Geolocation position unavailable, using default location'); 692 | } else if (error.code === 3) { 693 | console.log('Geolocation timeout, using default location'); 694 | } 695 | 696 | // Fallback to default location (Berlin) 697 | console.log('Falling back to default location...'); 698 | await this.loadDefaultLocation(); 699 | } 700 | }, 701 | 702 | async loadDefaultLocation() { 703 | console.log('Loading default location (Berlin)...'); 704 | appState.currentLocation = { 705 | name: 'Berlin', 706 | country: 'Germany', 707 | latitude: 52.52437, 708 | longitude: 13.41053 709 | }; 710 | 711 | try { 712 | await weather.loadWeatherData(52.52437, 13.41053); 713 | } catch (error) { 714 | console.error('Failed to load default location weather:', error); 715 | // Show sample data for demonstration purposes 716 | console.log('Showing sample data for demonstration...'); 717 | weather.showSampleData(); 718 | } 719 | } 720 | }; 721 | 722 | // Search functionality 723 | const search = { 724 | init() { 725 | elements.searchInput = document.getElementById('searchInput'); 726 | elements.searchButton = document.getElementById('searchButton'); 727 | elements.searchResults = document.getElementById('searchResults'); 728 | 729 | if (!elements.searchInput || !elements.searchButton || !elements.searchResults) { 730 | console.error('Search elements not found'); 731 | return; 732 | } 733 | 734 | // Debounced search function 735 | const debouncedSearch = utils.debounce(this.performSearch.bind(this), 300); 736 | 737 | // Event listeners 738 | elements.searchInput.addEventListener('input', (e) => { 739 | const query = e.target.value.trim(); 740 | if (query.length >= 2) { 741 | debouncedSearch(query); 742 | } else { 743 | this.hideResults(); 744 | } 745 | }); 746 | 747 | elements.searchButton.addEventListener('click', () => { 748 | const query = elements.searchInput.value.trim(); 749 | if (query) { 750 | this.performSearch(query); 751 | } 752 | }); 753 | 754 | elements.searchInput.addEventListener('keydown', (e) => { 755 | if (e.key === 'Enter') { 756 | e.preventDefault(); 757 | const query = elements.searchInput.value.trim(); 758 | if (query) { 759 | this.performSearch(query); 760 | } 761 | } else if (e.key === 'Escape') { 762 | this.hideResults(); 763 | } 764 | }); 765 | 766 | // Hide results when clicking outside 767 | document.addEventListener('click', (e) => { 768 | if (!elements.searchInput.contains(e.target) && !elements.searchResults.contains(e.target)) { 769 | this.hideResults(); 770 | } 771 | }); 772 | 773 | // Handle keyboard navigation in search results 774 | elements.searchInput.addEventListener('keydown', (e) => { 775 | const results = elements.searchResults.querySelectorAll('.search-result-item'); 776 | const activeResult = elements.searchResults.querySelector('.search-result-item.active'); 777 | let currentIndex = Array.from(results).indexOf(activeResult); 778 | 779 | if (e.key === 'ArrowDown') { 780 | e.preventDefault(); 781 | currentIndex = Math.min(currentIndex + 1, results.length - 1); 782 | this.highlightResult(results, currentIndex); 783 | } else if (e.key === 'ArrowUp') { 784 | e.preventDefault(); 785 | currentIndex = Math.max(currentIndex - 1, 0); 786 | this.highlightResult(results, currentIndex); 787 | } else if (e.key === 'Enter' && activeResult) { 788 | e.preventDefault(); 789 | activeResult.click(); 790 | } 791 | }); 792 | }, 793 | 794 | async performSearch(query) { 795 | try { 796 | utils.showLoading(elements.searchButton); 797 | const locations = await api.searchLocations(query); 798 | this.displayResults(locations); 799 | } catch (error) { 800 | console.error('Search failed:', error); 801 | this.showSearchError(); 802 | } finally { 803 | utils.hideLoading(elements.searchButton); 804 | } 805 | }, 806 | 807 | displayResults(locations) { 808 | if (!locations || locations.length === 0) { 809 | this.showNoResults(); 810 | return; 811 | } 812 | 813 | const resultsHTML = locations.map(location => ` 814 |
815 |
${location.name}
816 |
817 | ${location.admin1 ? location.admin1 + ', ' : ''}${location.country} 818 |
819 |
820 | `).join(''); 821 | 822 | elements.searchResults.innerHTML = resultsHTML; 823 | this.showResults(); 824 | 825 | // Add click listeners to results 826 | elements.searchResults.querySelectorAll('.search-result-item').forEach(item => { 827 | item.addEventListener('click', () => { 828 | const location = JSON.parse(item.dataset.location); 829 | this.selectLocation(location); 830 | }); 831 | }); 832 | }, 833 | 834 | selectLocation(location) { 835 | appState.currentLocation = location; 836 | elements.searchInput.value = `${location.name}, ${location.country}`; 837 | this.hideResults(); 838 | 839 | // Load weather for selected location 840 | weather.loadWeatherData(location.latitude, location.longitude); 841 | }, 842 | 843 | showResults() { 844 | elements.searchResults.classList.add('active'); 845 | }, 846 | 847 | hideResults() { 848 | elements.searchResults.classList.remove('active'); 849 | }, 850 | 851 | showNoResults() { 852 | elements.searchResults.innerHTML = ` 853 |
854 |
No results found
855 |
Try a different search term
856 |
857 | `; 858 | this.showResults(); 859 | }, 860 | 861 | showSearchError() { 862 | elements.searchResults.innerHTML = ` 863 |
864 |
Search failed
865 |
Please try again
866 |
867 | `; 868 | this.showResults(); 869 | }, 870 | 871 | highlightResult(results, index) { 872 | results.forEach((result, i) => { 873 | if (i === index) { 874 | result.classList.add('active'); 875 | result.scrollIntoView({ block: 'nearest' }); 876 | } else { 877 | result.classList.remove('active'); 878 | } 879 | }); 880 | } 881 | }; 882 | 883 | // Weather data management 884 | const weather = { 885 | async loadWeatherData(latitude, longitude) { 886 | console.log('Loading weather data for:', latitude, longitude); 887 | try { 888 | this.showLoading(); 889 | const data = await api.getWeatherData(latitude, longitude); 890 | console.log('Weather data received:', data); 891 | 892 | if (data) { 893 | appState.weatherData = data; 894 | this.displayWeatherData(data); 895 | this.showWeatherData(); 896 | console.log('Weather data displayed successfully'); 897 | } else { 898 | console.log('No weather data received'); 899 | this.showError('Unable to load weather data. Please check your internet connection and try again.'); 900 | } 901 | } catch (error) { 902 | console.error('Weather loading failed:', error); 903 | this.showError('Unable to connect to weather service. Please check your internet connection and try again.'); 904 | } 905 | }, 906 | 907 | displayWeatherData(data) { 908 | // Update current weather 909 | this.updateCurrentWeather(data); 910 | 911 | // Update metrics 912 | this.updateMetrics(data); 913 | 914 | // Update forecasts 915 | this.updateDailyForecast(data); 916 | this.updateHourlyForecast(data); 917 | 918 | // Update favorites UI 919 | favorites.updateUI(); 920 | 921 | // Update weather animations 922 | if (data.current && data.current.weather_code !== undefined) { 923 | animations.setWeatherBackground(data.current.weather_code); 924 | } 925 | }, 926 | 927 | updateCurrentWeather(data) { 928 | const current = data.current; 929 | const location = appState.currentLocation; 930 | 931 | if (elements.currentLocation) { 932 | elements.currentLocation.textContent = location ? 933 | `${location.name}, ${location.country}` : 'Current Location'; 934 | } 935 | 936 | if (elements.currentDate) { 937 | elements.currentDate.textContent = new Date().toLocaleDateString('en-US', { 938 | weekday: 'long', 939 | month: 'long', 940 | day: 'numeric', 941 | year: 'numeric' 942 | }); 943 | } 944 | 945 | if (elements.currentTemp) { 946 | elements.currentTemp.textContent = utils.formatTemperature(current.temperature_2m); 947 | } 948 | 949 | if (elements.currentWeatherIcon) { 950 | elements.currentWeatherIcon.src = utils.getWeatherIcon(current.weather_code); 951 | elements.currentWeatherIcon.alt = utils.getWeatherDescription(current.weather_code); 952 | } 953 | 954 | if (elements.currentDescription) { 955 | elements.currentDescription.textContent = utils.getWeatherDescription(current.weather_code); 956 | } 957 | }, 958 | 959 | updateMetrics(data) { 960 | const current = data.current; 961 | const daily = data.daily; 962 | 963 | if (elements.feelsLike) { 964 | elements.feelsLike.textContent = utils.formatTemperature(current.apparent_temperature); 965 | } 966 | 967 | if (elements.humidity) { 968 | elements.humidity.textContent = `${Math.round(current.relative_humidity_2m)}%`; 969 | } 970 | 971 | if (elements.windSpeed) { 972 | elements.windSpeed.textContent = utils.formatWindSpeed(current.wind_speed_10m); 973 | } 974 | 975 | if (elements.precipitation) { 976 | elements.precipitation.textContent = utils.formatPrecipitation(current.precipitation || 0); 977 | } 978 | 979 | // Additional metrics 980 | if (elements.uvIndex && daily && daily.uv_index_max) { 981 | elements.uvIndex.textContent = Math.round(daily.uv_index_max[0]); 982 | } 983 | 984 | if (elements.visibility && current.visibility) { 985 | const visibilityKm = (current.visibility / 1000).toFixed(1); 986 | elements.visibility.textContent = `${visibilityKm} km`; 987 | } 988 | 989 | if (elements.pressure) { 990 | elements.pressure.textContent = `${Math.round(current.surface_pressure)} hPa`; 991 | } 992 | 993 | // Sun times 994 | if (elements.sunrise && elements.sunset && daily) { 995 | const sunrise = new Date(daily.sunrise[0]); 996 | const sunset = new Date(daily.sunset[0]); 997 | 998 | elements.sunrise.textContent = sunrise.toLocaleTimeString('en-US', { 999 | hour: 'numeric', 1000 | minute: '2-digit', 1001 | hour12: true 1002 | }); 1003 | 1004 | elements.sunset.textContent = sunset.toLocaleTimeString('en-US', { 1005 | hour: 'numeric', 1006 | minute: '2-digit', 1007 | hour12: true 1008 | }); 1009 | } 1010 | }, 1011 | 1012 | updateDailyForecast(data) { 1013 | const daily = data.daily; 1014 | if (!elements.forecastDays || !daily) return; 1015 | 1016 | const forecastHTML = daily.time.slice(0, 7).map((date, index) => ` 1017 |
1018 |
${utils.formatDate(date).split(',')[0]}
1019 | ${utils.getWeatherDescription(daily.weather_code[index])} 1022 |
1023 | ${utils.formatTemperature(daily.temperature_2m_max[index])} 1024 | ${utils.formatTemperature(daily.temperature_2m_min[index])} 1025 |
1026 |
1027 | `).join(''); 1028 | 1029 | elements.forecastDays.innerHTML = forecastHTML; 1030 | 1031 | // Add click listeners for day selection 1032 | elements.forecastDays.querySelectorAll('.forecast-day').forEach(day => { 1033 | day.addEventListener('click', () => { 1034 | const dayIndex = parseInt(day.dataset.day); 1035 | appState.selectedDay = dayIndex; 1036 | this.updateHourlyForecast(data, dayIndex); 1037 | this.updateDaySelector(dayIndex); 1038 | }); 1039 | }); 1040 | }, 1041 | 1042 | updateHourlyForecast(data, selectedDay = 0) { 1043 | const hourly = data.hourly; 1044 | if (!elements.forecastHours || !hourly) return; 1045 | 1046 | // Get hours for selected day (24 hours starting from selected day) 1047 | const startIndex = selectedDay * 24; 1048 | const endIndex = startIndex + 24; 1049 | 1050 | const hoursHTML = hourly.time.slice(startIndex, endIndex).map((time, index) => { 1051 | const actualIndex = startIndex + index; 1052 | return ` 1053 |
1054 |
${utils.formatTime(time)}
1055 | ${utils.getWeatherDescription(hourly.weather_code[actualIndex])} 1058 |
${utils.formatTemperature(hourly.temperature_2m[actualIndex])}
1059 |
1060 | `; 1061 | }).join(''); 1062 | 1063 | elements.forecastHours.innerHTML = hoursHTML; 1064 | }, 1065 | 1066 | updateDaySelector(selectedDay) { 1067 | if (elements.daySelector) { 1068 | elements.daySelector.selectedIndex = selectedDay; 1069 | } 1070 | }, 1071 | 1072 | showLoading() { 1073 | if (elements.loadingState) elements.loadingState.classList.remove('hidden'); 1074 | if (elements.weatherData) elements.weatherData.classList.add('hidden'); 1075 | if (elements.errorState) elements.errorState.classList.add('hidden'); 1076 | }, 1077 | 1078 | showWeatherData() { 1079 | if (elements.loadingState) elements.loadingState.classList.add('hidden'); 1080 | if (elements.weatherData) elements.weatherData.classList.remove('hidden'); 1081 | if (elements.errorState) elements.errorState.classList.add('hidden'); 1082 | }, 1083 | 1084 | showError(message) { 1085 | console.log('Showing error:', message); 1086 | console.log('Elements available:', { 1087 | loadingState: !!elements.loadingState, 1088 | weatherData: !!elements.weatherData, 1089 | errorState: !!elements.errorState 1090 | }); 1091 | 1092 | if (elements.loadingState) elements.loadingState.classList.add('hidden'); 1093 | if (elements.weatherData) elements.weatherData.classList.add('hidden'); 1094 | if (elements.errorState) { 1095 | elements.errorState.classList.remove('hidden'); 1096 | const errorMessage = elements.errorState.querySelector('#errorMessage'); 1097 | if (errorMessage) { 1098 | errorMessage.textContent = message; 1099 | console.log('Error message updated to:', message); 1100 | } else { 1101 | console.log('Error message element not found'); 1102 | } 1103 | } else { 1104 | console.log('Error state element not found'); 1105 | } 1106 | }, 1107 | 1108 | showSampleData() { 1109 | console.log('Showing sample data for demonstration...'); 1110 | 1111 | // Hide loading and error states 1112 | if (elements.loadingState) elements.loadingState.classList.add('hidden'); 1113 | if (elements.errorState) elements.errorState.classList.add('hidden'); 1114 | 1115 | // Show weather data 1116 | if (elements.weatherData) elements.weatherData.classList.remove('hidden'); 1117 | 1118 | // Update current weather with sample data 1119 | if (elements.currentLocation) elements.currentLocation.textContent = 'Berlin, Germany'; 1120 | if (elements.currentDate) elements.currentDate.textContent = new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric', year: 'numeric' }); 1121 | if (elements.currentTemp) elements.currentTemp.textContent = '20°'; 1122 | if (elements.currentDescription) elements.currentDescription.textContent = 'Clear sky'; 1123 | 1124 | // Update metrics with sample data 1125 | if (elements.feelsLike) elements.feelsLike.textContent = '18°'; 1126 | if (elements.humidity) elements.humidity.textContent = '65%'; 1127 | if (elements.windSpeed) elements.windSpeed.textContent = '12 km/h'; 1128 | if (elements.precipitation) elements.precipitation.textContent = '0%'; 1129 | if (elements.uvIndex) elements.uvIndex.textContent = '5'; 1130 | if (elements.visibility) elements.visibility.textContent = '10 km'; 1131 | if (elements.pressure) elements.pressure.textContent = '1013 hPa'; 1132 | if (elements.sunrise) elements.sunrise.textContent = '6:30 AM'; 1133 | if (elements.sunset) elements.sunset.textContent = '8:15 PM'; 1134 | 1135 | // Show sample hourly forecast 1136 | this.showSampleHourlyForecast(); 1137 | 1138 | // Show sample daily forecast 1139 | this.showSampleDailyForecast(); 1140 | }, 1141 | 1142 | showSampleHourlyForecast() { 1143 | if (!elements.forecastHours) return; 1144 | 1145 | const sampleHours = [ 1146 | { time: '3 PM', temp: '20°', icon: './assets/images/icon-sunny.webp', alt: 'Sunny' }, 1147 | { time: '4 PM', temp: '21°', icon: './assets/images/icon-sunny.webp', alt: 'Sunny' }, 1148 | { time: '5 PM', temp: '20°', icon: './assets/images/icon-overcast.webp', alt: 'Overcast' }, 1149 | { time: '6 PM', temp: '19°', icon: './assets/images/icon-overcast.webp', alt: 'Overcast' }, 1150 | { time: '7 PM', temp: '18°', icon: './assets/images/icon-overcast.webp', alt: 'Overcast' }, 1151 | { time: '8 PM', temp: '17°', icon: './assets/images/icon-overcast.webp', alt: 'Overcast' }, 1152 | { time: '9 PM', temp: '16°', icon: './assets/images/icon-overcast.webp', alt: 'Overcast' }, 1153 | { time: '10 PM', temp: '15°', icon: './assets/images/icon-overcast.webp', alt: 'Overcast' } 1154 | ]; 1155 | 1156 | const hoursHTML = sampleHours.map(hour => ` 1157 |
1158 |
${hour.time}
1159 | ${hour.alt} 1160 |
${hour.temp}
1161 |
1162 | `).join(''); 1163 | 1164 | elements.forecastHours.innerHTML = hoursHTML; 1165 | }, 1166 | 1167 | showSampleDailyForecast() { 1168 | if (!elements.forecastDays) return; 1169 | 1170 | const sampleDays = [ 1171 | { day: 'Today', high: '20°', low: '12°', icon: './assets/images/icon-sunny.webp', alt: 'Sunny' }, 1172 | { day: 'Wed', high: '22°', low: '14°', icon: './assets/images/icon-overcast.webp', alt: 'Overcast' }, 1173 | { day: 'Thu', high: '18°', low: '10°', icon: './assets/images/icon-rainy.webp', alt: 'Rainy' }, 1174 | { day: 'Fri', high: '25°', low: '16°', icon: './assets/images/icon-sunny.webp', alt: 'Sunny' }, 1175 | { day: 'Sat', high: '23°', low: '15°', icon: './assets/images/icon-overcast.webp', alt: 'Overcast' }, 1176 | { day: 'Sun', high: '21°', low: '13°', icon: './assets/images/icon-overcast.webp', alt: 'Overcast' }, 1177 | { day: 'Mon', high: '19°', low: '11°', icon: './assets/images/icon-rainy.webp', alt: 'Rainy' } 1178 | ]; 1179 | 1180 | const daysHTML = sampleDays.map((day, index) => ` 1181 |
1182 |
${day.day}
1183 | ${day.alt} 1184 |
1185 | ${day.high} 1186 | ${day.low} 1187 |
1188 |
1189 | `).join(''); 1190 | 1191 | elements.forecastDays.innerHTML = daysHTML; 1192 | } 1193 | }; 1194 | 1195 | // Units management 1196 | const units = { 1197 | init() { 1198 | // Load saved units from localStorage 1199 | const savedUnits = localStorage.getItem('weatherAppUnits'); 1200 | if (savedUnits) { 1201 | appState.units = { ...appState.units, ...JSON.parse(savedUnits) }; 1202 | } 1203 | 1204 | this.updateUI(); 1205 | this.bindEvents(); 1206 | }, 1207 | 1208 | bindEvents() { 1209 | // Listen for unit changes 1210 | const unitInputs = document.querySelectorAll('input[name="system"], input[name="temperature"], input[name="windSpeed"], input[name="precipitation"]'); 1211 | 1212 | unitInputs.forEach(input => { 1213 | input.addEventListener('change', (e) => { 1214 | this.handleUnitChange(e.target.name, e.target.value); 1215 | }); 1216 | }); 1217 | }, 1218 | 1219 | handleUnitChange(unitType, value) { 1220 | // Handle system-wide changes 1221 | if (unitType === 'system') { 1222 | if (value === 'imperial') { 1223 | appState.units.temperature = 'fahrenheit'; 1224 | appState.units.windSpeed = 'mph'; 1225 | appState.units.precipitation = 'inches'; 1226 | } else { 1227 | appState.units.temperature = 'celsius'; 1228 | appState.units.windSpeed = 'kmh'; 1229 | appState.units.precipitation = 'mm'; 1230 | } 1231 | } else { 1232 | // Handle individual unit changes 1233 | appState.units[unitType] = value; 1234 | } 1235 | 1236 | this.updateUI(); 1237 | this.saveUnits(); 1238 | 1239 | // Refresh weather display if data is available 1240 | if (appState.weatherData) { 1241 | weather.displayWeatherData(appState.weatherData); 1242 | } 1243 | }, 1244 | 1245 | updateUI() { 1246 | // Update radio button states 1247 | const systemRadios = document.querySelectorAll('input[name="system"]'); 1248 | const tempRadios = document.querySelectorAll('input[name="temperature"]'); 1249 | const windRadios = document.querySelectorAll('input[name="windSpeed"]'); 1250 | const precipRadios = document.querySelectorAll('input[name="precipitation"]'); 1251 | 1252 | // Determine system setting 1253 | const isImperial = appState.units.temperature === 'fahrenheit' && 1254 | appState.units.windSpeed === 'mph' && 1255 | appState.units.precipitation === 'inches'; 1256 | 1257 | systemRadios.forEach(radio => { 1258 | if (radio.value === 'imperial' && isImperial) { 1259 | radio.checked = true; 1260 | } else if (radio.value === 'metric' && !isImperial) { 1261 | radio.checked = true; 1262 | } 1263 | }); 1264 | 1265 | // Update individual unit radios 1266 | tempRadios.forEach(radio => { 1267 | radio.checked = radio.value === appState.units.temperature; 1268 | }); 1269 | 1270 | windRadios.forEach(radio => { 1271 | radio.checked = radio.value === appState.units.windSpeed; 1272 | }); 1273 | 1274 | precipRadios.forEach(radio => { 1275 | radio.checked = radio.value === appState.units.precipitation; 1276 | }); 1277 | }, 1278 | 1279 | saveUnits() { 1280 | localStorage.setItem('weatherAppUnits', JSON.stringify(appState.units)); 1281 | } 1282 | }; 1283 | 1284 | // Initialize DOM elements 1285 | const initializeElements = () => { 1286 | console.log('Initializing DOM elements...'); 1287 | 1288 | // Weather display elements 1289 | elements.currentLocation = document.getElementById('currentLocation'); 1290 | elements.currentDate = document.getElementById('currentDate'); 1291 | elements.currentTemp = document.getElementById('currentTemp'); 1292 | elements.currentWeatherIcon = document.getElementById('currentWeatherIcon'); 1293 | elements.currentDescription = document.getElementById('currentDescription'); 1294 | 1295 | // Metrics elements 1296 | elements.feelsLike = document.getElementById('feelsLike'); 1297 | elements.humidity = document.getElementById('humidity'); 1298 | elements.windSpeed = document.getElementById('windSpeed'); 1299 | elements.precipitation = document.getElementById('precipitation'); 1300 | elements.uvIndex = document.getElementById('uvIndex'); 1301 | elements.visibility = document.getElementById('visibility'); 1302 | elements.pressure = document.getElementById('pressure'); 1303 | elements.sunrise = document.getElementById('sunrise'); 1304 | elements.sunset = document.getElementById('sunset'); 1305 | 1306 | // Forecast elements 1307 | elements.forecastDays = document.getElementById('forecastDays'); 1308 | elements.forecastHours = document.getElementById('forecastHours'); 1309 | elements.daySelector = document.getElementById('daySelector'); 1310 | 1311 | // State elements 1312 | elements.loadingState = document.getElementById('loadingState'); 1313 | elements.weatherData = document.getElementById('weatherData'); 1314 | elements.errorState = document.getElementById('errorState'); 1315 | elements.retryButton = document.getElementById('retryButton'); 1316 | 1317 | // Units elements 1318 | elements.unitsButton = document.getElementById('unitsButton'); 1319 | elements.unitsMenu = document.getElementById('unitsMenu'); 1320 | 1321 | // Favorites elements 1322 | elements.favoriteButton = document.getElementById('favoriteButton'); 1323 | elements.favoritesButton = document.getElementById('favoritesButton'); 1324 | elements.favoritesMenu = document.getElementById('favoritesMenu'); 1325 | elements.favoritesList = document.getElementById('favoritesList'); 1326 | 1327 | // Debug: Check if critical elements are found 1328 | console.log('Critical elements found:'); 1329 | console.log('- loadingState:', !!elements.loadingState); 1330 | console.log('- weatherData:', !!elements.weatherData); 1331 | console.log('- errorState:', !!elements.errorState); 1332 | console.log('- retryButton:', !!elements.retryButton); 1333 | }; 1334 | 1335 | // Initialize app when DOM is loaded 1336 | document.addEventListener('DOMContentLoaded', () => { 1337 | console.log('DOM loaded, initializing weather app...'); 1338 | 1339 | // Initialize DOM elements 1340 | initializeElements(); 1341 | console.log('DOM elements initialized'); 1342 | 1343 | // Initialize components 1344 | theme.init(); 1345 | console.log('Theme initialized'); 1346 | 1347 | search.init(); 1348 | console.log('Search initialized'); 1349 | 1350 | units.init(); 1351 | console.log('Units initialized'); 1352 | 1353 | favorites.init(); 1354 | console.log('Favorites initialized'); 1355 | 1356 | animations.init(); 1357 | console.log('Animations initialized'); 1358 | 1359 | // Initialize units dropdown 1360 | if (elements.unitsButton && elements.unitsMenu) { 1361 | elements.unitsButton.addEventListener('click', () => { 1362 | const isOpen = elements.unitsMenu.classList.contains('active'); 1363 | if (isOpen) { 1364 | elements.unitsMenu.classList.remove('active'); 1365 | elements.unitsButton.setAttribute('aria-expanded', 'false'); 1366 | } else { 1367 | elements.unitsMenu.classList.add('active'); 1368 | elements.unitsButton.setAttribute('aria-expanded', 'true'); 1369 | } 1370 | }); 1371 | 1372 | // Close units menu when clicking outside 1373 | document.addEventListener('click', (e) => { 1374 | if (!elements.unitsButton.contains(e.target) && !elements.unitsMenu.contains(e.target)) { 1375 | elements.unitsMenu.classList.remove('active'); 1376 | elements.unitsButton.setAttribute('aria-expanded', 'false'); 1377 | } 1378 | }); 1379 | } 1380 | 1381 | // Initialize day selector 1382 | if (elements.daySelector) { 1383 | elements.daySelector.addEventListener('change', (e) => { 1384 | const selectedDay = parseInt(e.target.value); 1385 | appState.selectedDay = selectedDay; 1386 | if (appState.weatherData) { 1387 | weather.updateHourlyForecast(appState.weatherData, selectedDay); 1388 | } 1389 | }); 1390 | } 1391 | 1392 | // Initialize retry button 1393 | if (elements.retryButton) { 1394 | elements.retryButton.addEventListener('click', () => { 1395 | if (appState.currentLocation) { 1396 | weather.loadWeatherData(appState.currentLocation.latitude, appState.currentLocation.longitude); 1397 | } else { 1398 | geolocation.loadCurrentLocationWeather(); 1399 | } 1400 | }); 1401 | } 1402 | 1403 | // Try to load current location weather 1404 | geolocation.loadCurrentLocationWeather().catch(error => { 1405 | console.error('Failed to initialize geolocation:', error); 1406 | }); 1407 | 1408 | console.log('Weather App initialized successfully'); 1409 | }); 1410 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* Weather App Styles */ 2 | 3 | /* Import Google Fonts */ 4 | @import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,300;0,500;0,600;1,600;0,700&family=Bricolage+Grotesque:wght@700&display=swap'); 5 | 6 | /* CSS Custom Properties (Design System) */ 7 | :root { 8 | /* Colors - Neutral */ 9 | --neutral-900: hsl(243, 96%, 9%); 10 | --neutral-800: hsl(243, 27%, 20%); 11 | --neutral-700: hsl(243, 23%, 24%); 12 | --neutral-600: hsl(243, 23%, 30%); 13 | --neutral-300: hsl(240, 6%, 70%); 14 | --neutral-200: hsl(250, 6%, 84%); 15 | --neutral-0: hsl(0, 0%, 100%); 16 | 17 | /* Colors - Orange */ 18 | --orange-500: hsl(28, 100%, 52%); 19 | 20 | /* Colors - Blue */ 21 | --blue-500: hsl(235, 50%, 50%); 22 | --blue-700: hsl(245, 60%, 40%); 23 | 24 | /* Typography */ 25 | --font-primary: 'DM Sans', sans-serif; 26 | --font-display: 'Bricolage Grotesque', sans-serif; 27 | --font-size-base: 18px; 28 | 29 | /* Spacing */ 30 | --spacing-xs: 0.5rem; 31 | --spacing-sm: 1rem; 32 | --spacing-md: 1.5rem; 33 | --spacing-lg: 2rem; 34 | --spacing-xl: 3rem; 35 | --spacing-2xl: 4rem; 36 | 37 | /* Border Radius */ 38 | --radius-sm: 8px; 39 | --radius-md: 12px; 40 | --radius-lg: 16px; 41 | --radius-xl: 24px; 42 | 43 | /* Shadows */ 44 | --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); 45 | --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); 46 | --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); 47 | 48 | /* Transitions */ 49 | --transition-fast: 150ms ease-in-out; 50 | --transition-normal: 250ms ease-in-out; 51 | --transition-slow: 350ms ease-in-out; 52 | } 53 | 54 | /* Dark mode variables */ 55 | [data-theme="dark"] { 56 | --bg-primary: hsl(230, 35%, 8%); 57 | --bg-secondary: hsl(235, 30%, 12%); 58 | --bg-tertiary: hsl(240, 25%, 16%); 59 | --text-primary: hsl(0, 0%, 100%); 60 | --text-secondary: hsl(240, 6%, 84%); 61 | --text-tertiary: hsl(240, 6%, 70%); 62 | --border-color: hsl(240, 25%, 20%); 63 | --shadow-color: rgba(0, 0, 0, 0.4); 64 | } 65 | 66 | /* Dark mode body background */ 67 | [data-theme="dark"] body { 68 | background: linear-gradient(135deg, hsl(230, 35%, 8%) 0%, hsl(235, 40%, 6%) 50%, hsl(240, 45%, 4%) 100%); 69 | } 70 | 71 | /* Light mode variables */ 72 | [data-theme="light"] { 73 | --bg-primary: hsl(0, 0%, 100%); 74 | --bg-secondary: hsl(240, 6%, 98%); 75 | --bg-tertiary: hsl(240, 6%, 95%); 76 | --text-primary: hsl(243, 96%, 9%); 77 | --text-secondary: hsl(243, 23%, 24%); 78 | --text-tertiary: hsl(243, 23%, 30%); 79 | --border-color: hsl(240, 6%, 90%); 80 | --shadow-color: rgba(0, 0, 0, 0.1); 81 | } 82 | 83 | /* Light mode body background */ 84 | [data-theme="light"] body { 85 | background: linear-gradient(135deg, hsl(0, 0%, 100%) 0%, hsl(240, 6%, 99%) 50%, hsl(240, 6%, 98%) 100%); 86 | } 87 | 88 | /* Default to dark mode */ 89 | :root { 90 | --bg-primary: hsl(230, 35%, 8%); 91 | --bg-secondary: hsl(235, 30%, 12%); 92 | --bg-tertiary: hsl(240, 25%, 16%); 93 | --text-primary: hsl(0, 0%, 100%); 94 | --text-secondary: hsl(240, 6%, 84%); 95 | --text-tertiary: hsl(240, 6%, 70%); 96 | --border-color: hsl(240, 25%, 20%); 97 | --shadow-color: rgba(0, 0, 0, 0.4); 98 | } 99 | 100 | /* Default body background (dark mode) */ 101 | body { 102 | background: linear-gradient(135deg, hsl(230, 35%, 8%) 0%, hsl(235, 40%, 6%) 50%, hsl(240, 45%, 4%) 100%); 103 | } 104 | 105 | /* Theme Transition */ 106 | * { 107 | transition: background-color var(--transition-normal), 108 | color var(--transition-normal), 109 | border-color var(--transition-normal), 110 | box-shadow var(--transition-normal); 111 | } 112 | 113 | /* Reset and Base Styles */ 114 | * { 115 | margin: 0; 116 | padding: 0; 117 | box-sizing: border-box; 118 | } 119 | 120 | html { 121 | font-size: var(--font-size-base); 122 | scroll-behavior: smooth; 123 | } 124 | 125 | body { 126 | font-family: var(--font-primary); 127 | color: var(--text-primary); 128 | line-height: 1.6; 129 | min-height: 100vh; 130 | transition: background var(--transition-normal), color var(--transition-normal); 131 | } 132 | 133 | /* Typography */ 134 | h1, h2, h3, h4, h5, h6 { 135 | font-family: var(--font-display); 136 | font-weight: 700; 137 | line-height: 1.2; 138 | margin-bottom: var(--spacing-sm); 139 | } 140 | 141 | h1 { 142 | font-size: 2.5rem; 143 | } 144 | 145 | h2 { 146 | font-size: 2rem; 147 | } 148 | 149 | h3 { 150 | font-size: 1.5rem; 151 | } 152 | 153 | p { 154 | margin-bottom: var(--spacing-sm); 155 | } 156 | 157 | /* Utility Classes */ 158 | .container { 159 | max-width: 1440px; 160 | margin: 0 auto; 161 | padding: 0 var(--spacing-md); 162 | position: relative; 163 | } 164 | 165 | .sr-only { 166 | position: absolute; 167 | width: 1px; 168 | height: 1px; 169 | padding: 0; 170 | margin: -1px; 171 | overflow: hidden; 172 | clip: rect(0, 0, 0, 0); 173 | white-space: nowrap; 174 | border: 0; 175 | } 176 | 177 | .loading { 178 | opacity: 0.6; 179 | pointer-events: none; 180 | } 181 | 182 | .hidden { 183 | display: none !important; 184 | } 185 | 186 | .fade-in { 187 | animation: fadeIn var(--transition-normal) ease-in-out; 188 | } 189 | 190 | @keyframes fadeIn { 191 | from { 192 | opacity: 0; 193 | transform: translateY(10px); 194 | } 195 | to { 196 | opacity: 1; 197 | transform: translateY(0); 198 | } 199 | } 200 | 201 | /* Button Styles */ 202 | .btn { 203 | display: inline-flex; 204 | align-items: center; 205 | justify-content: center; 206 | padding: var(--spacing-xs) var(--spacing-md); 207 | border: none; 208 | border-radius: var(--radius-sm); 209 | font-family: var(--font-primary); 210 | font-size: 1rem; 211 | font-weight: 500; 212 | text-decoration: none; 213 | cursor: pointer; 214 | transition: all var(--transition-fast); 215 | gap: var(--spacing-xs); 216 | } 217 | 218 | .btn:focus { 219 | outline: 2px solid var(--blue-500); 220 | outline-offset: 2px; 221 | } 222 | 223 | .btn-primary { 224 | background-color: var(--blue-500); 225 | color: var(--neutral-0); 226 | } 227 | 228 | .btn-primary:hover { 229 | background-color: var(--blue-700); 230 | transform: translateY(-1px); 231 | } 232 | 233 | .btn-secondary { 234 | background-color: var(--bg-secondary); 235 | color: var(--text-primary); 236 | border: 1px solid var(--bg-tertiary); 237 | } 238 | 239 | .btn-secondary:hover { 240 | background-color: var(--bg-tertiary); 241 | transform: translateY(-1px); 242 | } 243 | 244 | /* Input Styles */ 245 | .input { 246 | width: 100%; 247 | padding: var(--spacing-sm) var(--spacing-md); 248 | border: 1px solid var(--bg-tertiary); 249 | border-radius: var(--radius-sm); 250 | background-color: var(--bg-secondary); 251 | color: var(--text-primary); 252 | font-family: var(--font-primary); 253 | font-size: 1rem; 254 | transition: all var(--transition-fast); 255 | } 256 | 257 | .input:focus { 258 | outline: none; 259 | border-color: var(--blue-500); 260 | box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); 261 | } 262 | 263 | .input::placeholder { 264 | color: var(--text-tertiary); 265 | } 266 | 267 | /* Card Styles */ 268 | .card { 269 | background: var(--bg-secondary); 270 | border-radius: var(--radius-lg); 271 | padding: var(--spacing-lg); 272 | box-shadow: var(--shadow-md); 273 | transition: all var(--transition-normal); 274 | animation: slideInUp 0.6s ease-out; 275 | /* border: 1px solid hsl(240, 20%, 25%); */ 276 | } 277 | 278 | .card:hover { 279 | transform: translateY(-4px) scale(1.02); 280 | box-shadow: var(--shadow-lg); 281 | } 282 | 283 | /* Staggered animation for cards */ 284 | .metric-card:nth-child(1) { animation-delay: 0.1s; } 285 | .metric-card:nth-child(2) { animation-delay: 0.2s; } 286 | .metric-card:nth-child(3) { animation-delay: 0.3s; } 287 | .metric-card:nth-child(4) { animation-delay: 0.4s; } 288 | .metric-card:nth-child(5) { animation-delay: 0.5s; } 289 | .metric-card:nth-child(6) { animation-delay: 0.6s; } 290 | .metric-card:nth-child(7) { animation-delay: 0.7s; } 291 | .metric-card:nth-child(8) { animation-delay: 0.8s; } 292 | 293 | @keyframes slideInUp { 294 | from { 295 | opacity: 0; 296 | transform: translateY(30px); 297 | } 298 | to { 299 | opacity: 1; 300 | transform: translateY(0); 301 | } 302 | } 303 | 304 | /* Pulse animation for loading */ 305 | .loading .card { 306 | animation: pulse 1.5s ease-in-out infinite; 307 | } 308 | 309 | @keyframes pulse { 310 | 0%, 100% { 311 | opacity: 1; 312 | } 313 | 50% { 314 | opacity: 0.7; 315 | } 316 | } 317 | 318 | /* Responsive Design System */ 319 | 320 | /* Large Desktop (1200px+) */ 321 | @media (min-width: 1200px) { 322 | .weather-data { 323 | max-width: 1400px; 324 | grid-template-columns: 1fr 450px; 325 | gap: var(--spacing-2xl); 326 | } 327 | 328 | .main-title { 329 | font-weight: 700; 330 | margin-bottom: var(--spacing-2xl); 331 | } 332 | 333 | .current-weather { 334 | padding: var(--spacing-2xl); 335 | min-height: 280px; 336 | } 337 | 338 | .current-temp { 339 | font-size: 5rem; 340 | } 341 | 342 | .current-weather-icon { 343 | width: 100px; 344 | height: 100px; 345 | } 346 | 347 | .weather-metrics { 348 | grid-template-columns: repeat(3, 1fr); 349 | gap: var(--spacing-lg); 350 | } 351 | 352 | .metric-card { 353 | padding: var(--spacing-lg); 354 | min-height: 120px; 355 | } 356 | 357 | .daily-forecast { 358 | padding: var(--spacing-2xl); 359 | min-height: 500px; 360 | } 361 | 362 | .forecast-day { 363 | min-width: 140px; 364 | padding: var(--spacing-lg); 365 | min-height: 200px; 366 | } 367 | 368 | .day-icon { 369 | width: 70px; 370 | height: 70px; 371 | } 372 | 373 | .hourly-forecast { 374 | padding: var(--spacing-2xl); 375 | } 376 | 377 | .forecast-hours { 378 | max-height: 500px; 379 | } 380 | 381 | .forecast-hour { 382 | padding: var(--spacing-md) 0; 383 | min-height: 60px; 384 | } 385 | 386 | .hour-icon { 387 | width: 32px; 388 | height: 32px; 389 | } 390 | } 391 | 392 | /* Desktop (1024px - 1199px) */ 393 | @media (max-width: 1199px) and (min-width: 1024px) { 394 | .weather-data { 395 | max-width: 1200px; 396 | grid-template-columns: 1fr 400px; 397 | gap: var(--spacing-xl); 398 | } 399 | 400 | .container { 401 | max-width: 1200px; 402 | padding: 0 var(--spacing-xl); 403 | } 404 | 405 | .main-title { 406 | font-size: 2.5rem; 407 | margin-bottom: var(--spacing-xl); 408 | } 409 | 410 | .current-weather { 411 | padding: var(--spacing-xl); 412 | min-height: 240px; 413 | } 414 | 415 | .current-temp { 416 | font-size: 4rem; 417 | } 418 | 419 | .current-weather-icon { 420 | width: 90px; 421 | height: 90px; 422 | } 423 | 424 | .weather-metrics { 425 | grid-template-columns: repeat(3, 1fr); 426 | gap: var(--spacing-md); 427 | } 428 | 429 | .metric-card { 430 | padding: var(--spacing-md); 431 | min-height: 100px; 432 | } 433 | 434 | .daily-forecast { 435 | padding: var(--spacing-xl); 436 | min-height: 450px; 437 | } 438 | 439 | .forecast-day { 440 | min-width: 130px; 441 | padding: var(--spacing-md); 442 | min-height: 180px; 443 | } 444 | 445 | .day-icon { 446 | width: 60px; 447 | height: 60px; 448 | } 449 | 450 | .hourly-forecast { 451 | padding: var(--spacing-xl); 452 | } 453 | 454 | .forecast-hours { 455 | max-height: 450px; 456 | } 457 | 458 | .forecast-hour { 459 | padding: var(--spacing-sm) 0; 460 | min-height: 50px; 461 | } 462 | 463 | .hour-icon { 464 | width: 28px; 465 | height: 28px; 466 | } 467 | } 468 | 469 | /* Tablet (768px - 1023px) */ 470 | @media (max-width: 1023px) and (min-width: 768px) { 471 | .weather-data { 472 | grid-template-columns: 1fr; 473 | gap: var(--spacing-lg); 474 | max-width: 800px; 475 | padding: 0 var(--spacing-md); 476 | } 477 | 478 | .main-title { 479 | font-size: 2.5rem; 480 | text-align: center; 481 | margin-bottom: var(--spacing-xl); 482 | } 483 | 484 | .hourly-forecast { 485 | padding: var(--spacing-lg); 486 | margin-bottom: var(--spacing-lg); 487 | } 488 | 489 | .weather-metrics { 490 | grid-template-columns: repeat(4, 1fr); 491 | gap: var(--spacing-md); 492 | margin-bottom: var(--spacing-lg); 493 | } 494 | 495 | .current-weather { 496 | padding: var(--spacing-xl); 497 | border-radius: var(--radius-xl); 498 | } 499 | 500 | .daily-forecast { 501 | padding: var(--spacing-lg); 502 | } 503 | 504 | .forecast-days { 505 | display: flex; 506 | gap: var(--spacing-sm); 507 | overflow-x: auto; 508 | padding: var(--spacing-sm) 0; 509 | } 510 | 511 | .forecast-day { 512 | min-width: 120px; 513 | flex-shrink: 0; 514 | } 515 | 516 | .header { 517 | margin-bottom: var(--spacing-xl); 518 | } 519 | } 520 | 521 | /* Mobile Large (480px - 767px) */ 522 | @media (max-width: 767px) and (min-width: 480px) { 523 | /* Enhanced mobile optimizations for larger screens */ 524 | * { 525 | -webkit-tap-highlight-color: transparent; 526 | -webkit-touch-callout: none; 527 | } 528 | 529 | html { 530 | -webkit-text-size-adjust: 100%; 531 | -ms-text-size-adjust: 100%; 532 | } 533 | 534 | body { 535 | -webkit-font-smoothing: antialiased; 536 | -moz-osx-font-smoothing: grayscale; 537 | text-rendering: optimizeLegibility; 538 | padding: 0; 539 | margin: 0; 540 | } 541 | 542 | .container { 543 | padding: 0 var(--spacing-lg); 544 | width: 100%; 545 | max-width: 100vw; 546 | overflow-x: hidden; 547 | } 548 | 549 | .weather-data { 550 | grid-template-columns: 1fr; 551 | gap: var(--spacing-xl); 552 | padding: 0 var(--spacing-lg); 553 | max-width: 100%; 554 | width: 100%; 555 | box-sizing: border-box; 556 | } 557 | 558 | .main-title { 559 | font-size: 2rem; 560 | margin-bottom: var(--spacing-xl); 561 | text-align: center; 562 | padding: 0 var(--spacing-md); 563 | line-height: 1.3; 564 | word-wrap: break-word; 565 | font-weight: 600; 566 | } 567 | 568 | .search-container { 569 | margin-bottom: var(--spacing-xl); 570 | width: 100%; 571 | display: flex; 572 | flex-direction: column; 573 | gap: var(--spacing-md); 574 | box-sizing: border-box; 575 | } 576 | 577 | .search-input-wrapper { 578 | display: flex; 579 | gap: var(--spacing-md); 580 | width: 100%; 581 | position: relative; 582 | box-sizing: border-box; 583 | align-items: center; 584 | } 585 | 586 | .search-icon { 587 | position: absolute; 588 | left: var(--spacing-md); 589 | width: 20px; 590 | height: 20px; 591 | opacity: 0.6; 592 | z-index: 2; 593 | pointer-events: none; 594 | } 595 | 596 | .search-input { 597 | width: 100%; 598 | font-size: 16px; /* Prevents zoom on iOS */ 599 | height: 60px; 600 | padding: var(--spacing-md) var(--spacing-md) var(--spacing-md) 3rem; 601 | border-radius: var(--radius-xl); 602 | -webkit-appearance: none; 603 | appearance: none; 604 | border: 1px solid var(--border-color); 605 | background: var(--bg-secondary); 606 | color: var(--text-primary); 607 | box-sizing: border-box; 608 | } 609 | 610 | .search-button { 611 | height: 60px; 612 | padding: 0 var(--spacing-xl); 613 | font-weight: 600; 614 | border-radius: var(--radius-xl); 615 | width: 100%; 616 | box-sizing: border-box; 617 | } 618 | 619 | .weather-metrics { 620 | grid-template-columns: repeat(2, 1fr); 621 | gap: var(--spacing-lg); 622 | margin-bottom: var(--spacing-xl); 623 | width: 100%; 624 | } 625 | 626 | .metric-card { 627 | min-height: 90px; 628 | display: flex; 629 | flex-direction: column; 630 | justify-content: center; 631 | } 632 | 633 | .card { 634 | padding: var(--spacing-lg); 635 | margin-bottom: var(--spacing-lg); 636 | border-radius: var(--radius-lg); 637 | width: 100%; 638 | box-sizing: border-box; 639 | } 640 | 641 | .current-weather { 642 | padding: var(--spacing-xl); 643 | border-radius: var(--radius-xl); 644 | min-height: 220px; 645 | display: flex; 646 | flex-direction: column; 647 | justify-content: space-between; 648 | } 649 | 650 | .current-temp { 651 | font-size: 3.5rem; 652 | line-height: 1; 653 | } 654 | 655 | .current-weather-icon { 656 | width: 80px; 657 | height: 80px; 658 | flex-shrink: 0; 659 | } 660 | 661 | .current-description { 662 | font-size: 1.1rem; 663 | line-height: 1.4; 664 | } 665 | 666 | .hourly-forecast { 667 | padding: var(--spacing-lg); 668 | width: 100%; 669 | } 670 | 671 | .forecast-hours { 672 | -webkit-overflow-scrolling: touch; 673 | } 674 | 675 | .daily-forecast { 676 | /* padding: var(--spacing-lg); */ 677 | width: 100%; 678 | } 679 | 680 | .forecast-days { 681 | display: flex; 682 | gap: var(--spacing-sm); 683 | overflow-x: auto; 684 | padding: var(--spacing-sm) 0; 685 | -webkit-overflow-scrolling: touch; 686 | scrollbar-width: thin; 687 | } 688 | 689 | .forecast-day { 690 | min-width: 110px; 691 | flex-shrink: 0; 692 | min-height: 160px; 693 | display: flex; 694 | flex-direction: column; 695 | align-items: center; 696 | justify-content: space-between; 697 | } 698 | 699 | .day-icon { 700 | width: 45px; 701 | height: 45px; 702 | } 703 | 704 | .header { 705 | margin-bottom: var(--spacing-lg); 706 | flex-wrap: nowrap; 707 | } 708 | 709 | .header-controls { 710 | gap: var(--spacing-sm); 711 | flex-shrink: 0; 712 | } 713 | 714 | .logo img { 715 | width: 120px; 716 | height: 60px; 717 | object-fit: contain; 718 | } 719 | 720 | /* Enhanced touch targets */ 721 | .btn { 722 | min-height: 56px; 723 | font-weight: 600; 724 | -webkit-appearance: none; 725 | appearance: none; 726 | touch-action: manipulation; 727 | } 728 | 729 | .units-button, 730 | .favorites-button { 731 | min-height: 56px; 732 | min-width: 56px; 733 | } 734 | 735 | .theme-toggle { 736 | width: 56px; 737 | height: 56px; 738 | } 739 | 740 | .forecast-day, 741 | .forecast-hour { 742 | min-height: 48px; 743 | touch-action: manipulation; 744 | } 745 | 746 | /* Mobile-specific improvements */ 747 | .app { 748 | padding: var(--spacing-md) 0; 749 | min-height: 100vh; 750 | min-height: -webkit-fill-available; 751 | } 752 | 753 | .weather-content { 754 | overflow-x: hidden; 755 | width: 100%; 756 | } 757 | 758 | /* Better text rendering */ 759 | .metric-label { 760 | font-size: 0.8rem; 761 | font-weight: 500; 762 | line-height: 1.2; 763 | } 764 | 765 | .metric-value { 766 | font-size: 1.3rem; 767 | font-weight: 700; 768 | line-height: 1.2; 769 | } 770 | 771 | .section-title { 772 | font-size: 1.1rem; 773 | font-weight: 600; 774 | } 775 | 776 | /* Hourly forecast improvements */ 777 | .hourly-header { 778 | flex-direction: row; 779 | justify-content: space-between; 780 | align-items: center; 781 | margin-bottom: var(--spacing-md); 782 | } 783 | 784 | .day-selector { 785 | height: 56px; 786 | font-size: 1rem; 787 | -webkit-appearance: none; 788 | appearance: none; 789 | } 790 | } 791 | 792 | /* Mobile Small (320px - 479px) */ 793 | @media (max-width: 479px) { 794 | /* Base mobile optimizations */ 795 | * { 796 | -webkit-tap-highlight-color: transparent; 797 | -webkit-touch-callout: none; 798 | } 799 | 800 | html { 801 | -webkit-text-size-adjust: 100%; 802 | -ms-text-size-adjust: 100%; 803 | } 804 | 805 | body { 806 | -webkit-font-smoothing: antialiased; 807 | -moz-osx-font-smoothing: grayscale; 808 | text-rendering: optimizeLegibility; 809 | padding: 0; 810 | margin: 0; 811 | } 812 | 813 | .container { 814 | padding: 0 var(--spacing-md); 815 | width: 100%; 816 | max-width: 100vw; 817 | overflow-x: hidden; 818 | } 819 | 820 | .weather-data { 821 | grid-template-columns: 1fr; 822 | gap: var(--spacing-lg); 823 | padding: 0 var(--spacing-md); 824 | max-width: 100%; 825 | width: 100%; 826 | box-sizing: border-box; 827 | } 828 | 829 | .main-title { 830 | font-size: 1.75rem; 831 | margin-bottom: var(--spacing-lg); 832 | text-align: center; 833 | padding: 0 var(--spacing-sm); 834 | line-height: 1.3; 835 | word-wrap: break-word; 836 | font-weight: 600; 837 | } 838 | 839 | .search-container { 840 | margin-bottom: var(--spacing-lg); 841 | width: 100%; 842 | display: flex; 843 | flex-direction: column; 844 | gap: var(--spacing-md); 845 | box-sizing: border-box; 846 | } 847 | 848 | .search-input-wrapper { 849 | display: flex; 850 | gap: var(--spacing-sm); 851 | align-items: center; 852 | width: 100%; 853 | position: relative; 854 | box-sizing: border-box; 855 | } 856 | 857 | .search-icon { 858 | position: absolute; 859 | left: var(--spacing-md); 860 | width: 20px; 861 | height: 20px; 862 | opacity: 0.6; 863 | z-index: 2; 864 | pointer-events: none; 865 | } 866 | 867 | .search-input { 868 | width: 100%; 869 | font-size: 16px; /* Prevents zoom on iOS */ 870 | height: 56px; 871 | padding: var(--spacing-md) var(--spacing-md) var(--spacing-md) 3rem; 872 | border-radius: var(--radius-lg); 873 | -webkit-appearance: none; 874 | appearance: none; 875 | border: 1px solid var(--border-color); 876 | background: var(--bg-secondary); 877 | color: var(--text-primary); 878 | font-family: var(--font-primary); 879 | transition: all var(--transition-fast); 880 | box-sizing: border-box; 881 | } 882 | 883 | .search-input:focus { 884 | outline: none; 885 | border-color: var(--blue-500); 886 | box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); 887 | } 888 | 889 | .search-input::placeholder { 890 | color: var(--text-tertiary); 891 | } 892 | 893 | .search-button { 894 | height: 56px; 895 | padding: 0 var(--spacing-lg); 896 | font-size: 1rem; 897 | font-weight: 600; 898 | border-radius: var(--radius-lg); 899 | width: 100%; 900 | box-sizing: border-box; 901 | } 902 | 903 | .weather-metrics { 904 | grid-template-columns: repeat(2, 1fr); 905 | gap: var(--spacing-md); 906 | margin-bottom: var(--spacing-lg); 907 | width: 100%; 908 | } 909 | 910 | .metric-card { 911 | padding: var(--spacing-lg); 912 | text-align: center; 913 | border-radius: var(--radius-lg); 914 | min-height: 100px; 915 | display: flex; 916 | flex-direction: column; 917 | justify-content: center; 918 | background: var(--bg-secondary); 919 | /* border: 1px solid var(--border-color); */ 920 | } 921 | 922 | .metric-label { 923 | font-size: 0.875rem; 924 | color: var(--text-secondary); 925 | margin-bottom: var(--spacing-xs); 926 | font-weight: 500; 927 | } 928 | 929 | .metric-value { 930 | font-size: 1.5rem; 931 | font-weight: 700; 932 | color: var(--text-primary); 933 | line-height: 1.2; 934 | } 935 | 936 | .card { 937 | padding: var(--spacing-lg); 938 | border-radius: var(--radius-lg); 939 | margin-bottom: var(--spacing-lg); 940 | width: 100%; 941 | box-sizing: border-box; 942 | background: var(--bg-secondary); 943 | border: 1px solid var(--border-color); 944 | } 945 | 946 | .current-weather { 947 | padding: var(--spacing-xl); 948 | margin-bottom: var(--spacing-lg); 949 | border-radius: var(--radius-xl); 950 | min-height: 240px; 951 | display: flex; 952 | flex-direction: column; 953 | justify-content: space-between; 954 | width: 100%; 955 | box-sizing: border-box; 956 | background: url('./assets/images/bg-today-large.svg') center/cover; 957 | color: var(--neutral-0); 958 | position: relative; 959 | overflow: hidden; 960 | } 961 | 962 | .current-weather-content { 963 | position: relative; 964 | z-index: 1; 965 | height: 100%; 966 | display: flex; 967 | flex-direction: column; 968 | justify-content: center; 969 | padding: var(--spacing-lg); 970 | } 971 | 972 | .current-weather-header { 973 | display: flex; 974 | flex-direction: row; 975 | align-items: flex-start; 976 | justify-content: space-between; 977 | margin-bottom: var(--spacing-lg); 978 | position: relative; 979 | z-index: 2; 980 | } 981 | 982 | .location-info { 983 | flex: 1; 984 | } 985 | 986 | .current-location { 987 | font-size: 1.25rem; 988 | font-weight: 600; 989 | margin-bottom: var(--spacing-xs); 990 | line-height: 1.2; 991 | color: var(--neutral-0); 992 | } 993 | 994 | .current-date { 995 | font-size: 0.875rem; 996 | color: rgba(255, 255, 255, 0.8); 997 | line-height: 1.4; 998 | font-weight: 500; 999 | } 1000 | 1001 | .current-temp-section { 1002 | display: flex; 1003 | flex-direction: row; 1004 | align-items: center; 1005 | justify-content: space-between; 1006 | margin: var(--spacing-lg) 0; 1007 | gap: var(--spacing-md); 1008 | } 1009 | 1010 | .current-temp { 1011 | font-size: 4rem; 1012 | font-weight: 700; 1013 | line-height: 1; 1014 | color: var(--neutral-0); 1015 | text-align: right; 1016 | } 1017 | 1018 | .current-weather-icon { 1019 | width: 80px; 1020 | height: 80px; 1021 | flex-shrink: 0; 1022 | order: -1; 1023 | } 1024 | 1025 | .current-description { 1026 | font-size: 1.125rem; 1027 | text-align: center; 1028 | margin-top: var(--spacing-md); 1029 | line-height: 1.4; 1030 | color: rgba(255, 255, 255, 0.9); 1031 | font-weight: 500; 1032 | } 1033 | 1034 | .favorite-button { 1035 | width: 48px; 1036 | height: 48px; 1037 | border-radius: var(--radius-md); 1038 | background: rgba(255, 255, 255, 0.1); 1039 | border: 1px solid rgba(255, 255, 255, 0.2); 1040 | display: flex; 1041 | align-items: center; 1042 | justify-content: center; 1043 | cursor: pointer; 1044 | transition: all var(--transition-fast); 1045 | backdrop-filter: blur(10px); 1046 | } 1047 | 1048 | .favorite-button:hover { 1049 | background: rgba(255, 255, 255, 0.2); 1050 | border-color: rgba(255, 255, 255, 0.3); 1051 | transform: translateY(-1px); 1052 | } 1053 | 1054 | .favorite-button.active { 1055 | background: var(--orange-500); 1056 | border-color: var(--orange-500); 1057 | } 1058 | 1059 | .favorite-icon { 1060 | width: 24px; 1061 | height: 24px; 1062 | filter: brightness(0) invert(1); 1063 | } 1064 | 1065 | .daily-forecast { 1066 | padding: var(--spacing-lg); 1067 | margin-bottom: var(--spacing-lg); 1068 | width: 100%; 1069 | background: var(--bg-secondary); 1070 | border-radius: var(--radius-lg); 1071 | box-shadow: var(--shadow-md); 1072 | } 1073 | 1074 | .forecast-days { 1075 | display: flex; 1076 | gap: var(--spacing-sm); 1077 | overflow-x: auto; 1078 | padding: var(--spacing-sm) 0; 1079 | -webkit-overflow-scrolling: touch; 1080 | scrollbar-width: none; 1081 | -ms-overflow-style: none; 1082 | } 1083 | 1084 | .forecast-days::-webkit-scrollbar { 1085 | display: none; 1086 | } 1087 | 1088 | .forecast-day { 1089 | padding: var(--spacing-md); 1090 | min-height: 160px; 1091 | min-width: 100px; 1092 | flex-shrink: 0; 1093 | border-radius: var(--radius-lg); 1094 | display: flex; 1095 | flex-direction: column; 1096 | align-items: center; 1097 | justify-content: space-between; 1098 | background: var(--bg-primary); 1099 | border: 1px solid var(--border-color); 1100 | transition: all var(--transition-fast); 1101 | } 1102 | 1103 | .forecast-day:hover { 1104 | background-color: var(--bg-tertiary); 1105 | transform: translateY(-2px); 1106 | } 1107 | 1108 | .day-name { 1109 | font-size: 0.875rem; 1110 | font-weight: 600; 1111 | color: var(--text-secondary); 1112 | margin-bottom: var(--spacing-sm); 1113 | } 1114 | 1115 | .day-icon { 1116 | width: 40px; 1117 | height: 40px; 1118 | margin: var(--spacing-sm) 0; 1119 | } 1120 | 1121 | .day-temps { 1122 | display: flex; 1123 | flex-direction: column; 1124 | align-items: center; 1125 | gap: var(--spacing-xs); 1126 | } 1127 | 1128 | .temp-high { 1129 | font-size: 1rem; 1130 | font-weight: 700; 1131 | color: var(--text-primary); 1132 | } 1133 | 1134 | .temp-low { 1135 | font-size: 0.875rem; 1136 | font-weight: 500; 1137 | color: var(--text-secondary); 1138 | } 1139 | 1140 | .hourly-forecast { 1141 | padding: var(--spacing-lg); 1142 | margin-bottom: var(--spacing-lg); 1143 | width: 100%; 1144 | background: var(--bg-secondary); 1145 | border-radius: var(--radius-lg); 1146 | box-shadow: var(--shadow-md); 1147 | display: flex; 1148 | flex-direction: column; 1149 | border: 1px solid var(--border-color); 1150 | } 1151 | 1152 | .hourly-header { 1153 | display: flex; 1154 | flex-direction: column; 1155 | gap: var(--spacing-md); 1156 | align-items: stretch; 1157 | margin-bottom: var(--spacing-lg); 1158 | padding-bottom: var(--spacing-md); 1159 | border-bottom: 1px solid var(--border-color); 1160 | } 1161 | 1162 | .day-selector { 1163 | width: 100%; 1164 | height: 56px; 1165 | padding: var(--spacing-md); 1166 | font-size: 1rem; 1167 | border-radius: var(--radius-lg); 1168 | -webkit-appearance: none; 1169 | appearance: none; 1170 | background: var(--bg-primary); 1171 | border: 1px solid var(--border-color); 1172 | color: var(--text-primary); 1173 | font-family: var(--font-primary); 1174 | cursor: pointer; 1175 | transition: all var(--transition-fast); 1176 | } 1177 | 1178 | .day-selector:focus { 1179 | outline: none; 1180 | border-color: var(--blue-500); 1181 | box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); 1182 | } 1183 | 1184 | .forecast-hours { 1185 | max-height: 300px; 1186 | overflow-y: auto; 1187 | -webkit-overflow-scrolling: touch; 1188 | display: flex; 1189 | flex-direction: column; 1190 | gap: var(--spacing-xs); 1191 | } 1192 | 1193 | .forecast-hour { 1194 | padding: var(--spacing-md) 0; 1195 | border-bottom: 1px solid var(--border-color); 1196 | display: flex; 1197 | align-items: center; 1198 | justify-content: space-between; 1199 | min-height: 48px; 1200 | transition: all var(--transition-fast); 1201 | } 1202 | 1203 | .forecast-hour:last-child { 1204 | border-bottom: none; 1205 | } 1206 | 1207 | .forecast-hour:hover { 1208 | background-color: var(--bg-tertiary); 1209 | } 1210 | 1211 | .hour-icon { 1212 | width: 24px; 1213 | height: 24px; 1214 | margin: 0 var(--spacing-md); 1215 | flex-shrink: 0; 1216 | } 1217 | 1218 | .hour-time { 1219 | font-size: 0.875rem; 1220 | font-weight: 500; 1221 | color: var(--text-secondary); 1222 | min-width: 60px; 1223 | } 1224 | 1225 | .hour-temp { 1226 | font-size: 1rem; 1227 | font-weight: 600; 1228 | color: var(--text-primary); 1229 | } 1230 | 1231 | /* Header adjustments for mobile */ 1232 | .header { 1233 | padding: var(--spacing-md) 0; 1234 | margin-bottom: var(--spacing-lg); 1235 | flex-wrap: nowrap; 1236 | display: flex; 1237 | justify-content: space-between; 1238 | align-items: center; 1239 | } 1240 | 1241 | .header-controls { 1242 | gap: var(--spacing-sm); 1243 | flex-shrink: 0; 1244 | display: flex; 1245 | align-items: center; 1246 | } 1247 | 1248 | .logo img { 1249 | width: 120px; 1250 | height: 60px; 1251 | object-fit: contain; 1252 | } 1253 | 1254 | /* Units and favorites dropdowns on mobile */ 1255 | .units-menu, 1256 | .favorites-menu { 1257 | position: fixed; 1258 | top: 50%; 1259 | left: 50%; 1260 | transform: translate(-50%, -50%); 1261 | width: 90vw; 1262 | max-width: 320px; 1263 | max-height: 70vh; 1264 | overflow-y: auto; 1265 | z-index: 1000; 1266 | border-radius: var(--radius-xl); 1267 | -webkit-overflow-scrolling: touch; 1268 | background: var(--bg-secondary); 1269 | border: 1px solid var(--border-color); 1270 | box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15); 1271 | } 1272 | 1273 | /* Better touch targets for mobile */ 1274 | .btn { 1275 | min-height: 56px; 1276 | padding: var(--spacing-md) var(--spacing-lg); 1277 | font-size: 1rem; 1278 | font-weight: 600; 1279 | border-radius: var(--radius-lg); 1280 | -webkit-appearance: none; 1281 | appearance: none; 1282 | touch-action: manipulation; 1283 | } 1284 | 1285 | .units-button, 1286 | .favorites-button { 1287 | min-height: 56px; 1288 | min-width: 56px; 1289 | padding: var(--spacing-md); 1290 | font-size: 0.875rem; 1291 | border-radius: var(--radius-lg); 1292 | } 1293 | 1294 | .theme-toggle { 1295 | width: 56px; 1296 | height: 56px; 1297 | border-radius: var(--radius-lg); 1298 | } 1299 | 1300 | .forecast-day, 1301 | .forecast-hour { 1302 | min-height: 48px; 1303 | touch-action: manipulation; 1304 | } 1305 | 1306 | /* Loading and error states on mobile */ 1307 | .loading-state, 1308 | .error-state { 1309 | padding: var(--spacing-xl); 1310 | text-align: center; 1311 | } 1312 | 1313 | .loading-icon, 1314 | .error-icon { 1315 | width: 48px; 1316 | height: 48px; 1317 | margin-bottom: var(--spacing-md); 1318 | } 1319 | 1320 | .error-state .btn { 1321 | width: 100%; 1322 | max-width: 240px; 1323 | margin: var(--spacing-lg) auto 0; 1324 | } 1325 | 1326 | /* Section titles */ 1327 | .section-title { 1328 | font-size: 1.125rem; 1329 | margin-bottom: var(--spacing-md); 1330 | font-weight: 600; 1331 | color: var(--text-primary); 1332 | } 1333 | 1334 | /* Hourly forecast header */ 1335 | .hourly-header { 1336 | display: flex; 1337 | flex-direction: row; 1338 | justify-content: space-between; 1339 | align-items: center; 1340 | margin-bottom: var(--spacing-lg); 1341 | } 1342 | 1343 | .day-selector { 1344 | height: 56px; 1345 | padding: var(--spacing-md); 1346 | font-size: 1rem; 1347 | border-radius: var(--radius-lg); 1348 | -webkit-appearance: none; 1349 | appearance: none; 1350 | background: var(--bg-primary); 1351 | border: 1px solid var(--border-color); 1352 | color: var(--text-primary); 1353 | min-width: 140px; 1354 | } 1355 | 1356 | /* Mobile-specific improvements */ 1357 | .app { 1358 | padding: var(--spacing-md) 0; 1359 | min-height: 100vh; 1360 | min-height: -webkit-fill-available; 1361 | } 1362 | 1363 | /* Prevent horizontal scroll */ 1364 | .weather-content { 1365 | overflow-x: hidden; 1366 | width: 100%; 1367 | } 1368 | 1369 | /* Location info styling */ 1370 | .current-location { 1371 | font-size: 1.25rem; 1372 | font-weight: 600; 1373 | color: var(--text-primary); 1374 | margin-bottom: var(--spacing-xs); 1375 | } 1376 | 1377 | .current-date { 1378 | font-size: 0.875rem; 1379 | color: var(--text-secondary); 1380 | font-weight: 500; 1381 | } 1382 | 1383 | /* Favorite button styling */ 1384 | .favorite-button { 1385 | width: 48px; 1386 | height: 48px; 1387 | border-radius: var(--radius-lg); 1388 | background: var(--bg-primary); 1389 | border: 1px solid var(--border-color); 1390 | display: flex; 1391 | align-items: center; 1392 | justify-content: center; 1393 | } 1394 | 1395 | .favorite-icon { 1396 | width: 20px; 1397 | height: 20px; 1398 | } 1399 | 1400 | /* Mobile layout order */ 1401 | .weather-left-column { 1402 | width: 100%; 1403 | order: 1; 1404 | } 1405 | 1406 | .hourly-forecast { 1407 | width: 100%; 1408 | order: 0; 1409 | } 1410 | 1411 | /* Improved spacing for very small screens */ 1412 | @media (max-width: 360px) { 1413 | .container { 1414 | padding: 0 var(--spacing-xs); 1415 | } 1416 | 1417 | .main-title { 1418 | font-size: 1.25rem; 1419 | } 1420 | 1421 | .current-temp { 1422 | font-size: 2.5rem; 1423 | } 1424 | 1425 | .current-weather-icon { 1426 | width: 60px; 1427 | height: 60px; 1428 | } 1429 | 1430 | .forecast-day { 1431 | min-width: 80px; 1432 | padding: var(--spacing-xs); 1433 | } 1434 | } 1435 | } 1436 | 1437 | /* Extra small screens (320px and below) */ 1438 | @media (max-width: 320px) { 1439 | .container { 1440 | padding: 0 var(--spacing-xs); 1441 | } 1442 | 1443 | .weather-data { 1444 | padding: 0 var(--spacing-xs); 1445 | gap: var(--spacing-xs); 1446 | } 1447 | 1448 | .card { 1449 | padding: var(--spacing-xs); 1450 | } 1451 | 1452 | .main-title { 1453 | font-size: 1.5rem; 1454 | } 1455 | 1456 | .current-temp { 1457 | font-size: 2rem; 1458 | } 1459 | 1460 | .metric-card { 1461 | padding: var(--spacing-xs); 1462 | } 1463 | 1464 | .forecast-hour, 1465 | .forecast-day { 1466 | padding: var(--spacing-xs); 1467 | } 1468 | } 1469 | 1470 | 1471 | 1472 | /* Weather App Specific Styles */ 1473 | 1474 | /* Main App Layout */ 1475 | .app { 1476 | min-height: 100vh; 1477 | background: linear-gradient(135deg, var(--neutral-900) 0%, var(--neutral-800) 100%); 1478 | padding: var(--spacing-lg) 0; 1479 | position: relative; 1480 | overflow-x: hidden; 1481 | transition: background var(--transition-slow); 1482 | } 1483 | 1484 | /* Weather-based backgrounds */ 1485 | .app.weather-clear { 1486 | background: linear-gradient(135deg, #1e3c72 0%, 100%); 1487 | } 1488 | 1489 | .app.weather-cloudy { 1490 | background: linear-gradient(135deg, 100%); 1491 | } 1492 | 1493 | .app.weather-rainy { 1494 | background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%); 1495 | } 1496 | 1497 | .app.weather-snowy { 1498 | background: linear-gradient(135deg, #e6ddd4 0%, #d5d4d0 100%); 1499 | } 1500 | 1501 | .app.weather-stormy { 1502 | background: linear-gradient(135deg, #232526 0%, #414345 100%); 1503 | } 1504 | 1505 | /* Animated background particles */ 1506 | .weather-particles { 1507 | position: absolute; 1508 | top: 0; 1509 | left: 0; 1510 | width: 100%; 1511 | height: 100%; 1512 | pointer-events: none; 1513 | overflow: hidden; 1514 | } 1515 | 1516 | .particle { 1517 | position: absolute; 1518 | background: rgba(255, 255, 255, 0.1); 1519 | border-radius: 50%; 1520 | animation: float 6s ease-in-out infinite; 1521 | } 1522 | 1523 | .particle.rain { 1524 | width: 2px; 1525 | height: 10px; 1526 | border-radius: 1px; 1527 | background: rgba(173, 216, 230, 0.6); 1528 | animation: rain 1s linear infinite; 1529 | } 1530 | 1531 | .particle.snow { 1532 | width: 4px; 1533 | height: 4px; 1534 | background: rgba(255, 255, 255, 0.8); 1535 | animation: snow 3s linear infinite; 1536 | } 1537 | 1538 | @keyframes float { 1539 | 0%, 100% { 1540 | transform: translateY(0px) rotate(0deg); 1541 | opacity: 0.5; 1542 | } 1543 | 50% { 1544 | transform: translateY(-20px) rotate(180deg); 1545 | opacity: 1; 1546 | } 1547 | } 1548 | 1549 | @keyframes rain { 1550 | 0% { 1551 | transform: translateY(-100vh) rotate(10deg); 1552 | opacity: 1; 1553 | } 1554 | 100% { 1555 | transform: translateY(100vh) rotate(10deg); 1556 | opacity: 0; 1557 | } 1558 | } 1559 | 1560 | @keyframes snow { 1561 | 0% { 1562 | transform: translateY(-100vh) rotate(0deg); 1563 | opacity: 1; 1564 | } 1565 | 100% { 1566 | transform: translateY(100vh) rotate(360deg); 1567 | opacity: 0; 1568 | } 1569 | } 1570 | 1571 | /* Header */ 1572 | .header { 1573 | display: flex; 1574 | justify-content: space-between; 1575 | align-items: center; 1576 | margin-bottom: var(--spacing-xl); 1577 | flex-wrap: nowrap; 1578 | gap: var(--spacing-md); 1579 | padding: var(--spacing-md) 0; 1580 | } 1581 | 1582 | .header-controls { 1583 | display: flex; 1584 | align-items: center; 1585 | gap: var(--spacing-md); 1586 | flex-shrink: 0; 1587 | } 1588 | 1589 | /* Theme Toggle */ 1590 | .theme-toggle { 1591 | display: flex; 1592 | align-items: center; 1593 | justify-content: center; 1594 | width: 48px; 1595 | height: 48px; 1596 | background-color: var(--bg-secondary); 1597 | border: 1px solid var(--border-color); 1598 | border-radius: var(--radius-lg); 1599 | cursor: pointer; 1600 | transition: all var(--transition-fast); 1601 | position: relative; 1602 | overflow: hidden; 1603 | } 1604 | 1605 | .theme-toggle:hover { 1606 | background-color: var(--bg-tertiary); 1607 | transform: translateY(-1px); 1608 | box-shadow: 0 4px 12px var(--shadow-color); 1609 | } 1610 | 1611 | .theme-toggle:active { 1612 | transform: translateY(0) scale(0.95); 1613 | } 1614 | 1615 | .theme-toggle:focus { 1616 | outline: 2px solid var(--blue-500); 1617 | outline-offset: 2px; 1618 | } 1619 | 1620 | .theme-toggle:focus-visible { 1621 | outline: 3px solid var(--blue-500); 1622 | outline-offset: 2px; 1623 | box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1); 1624 | } 1625 | 1626 | .theme-toggle::before { 1627 | content: ''; 1628 | position: absolute; 1629 | top: 50%; 1630 | left: 50%; 1631 | width: 0; 1632 | height: 0; 1633 | background: var(--bg-tertiary); 1634 | border-radius: 50%; 1635 | transform: translate(-50%, -50%); 1636 | transition: all var(--transition-fast); 1637 | z-index: 0; 1638 | } 1639 | 1640 | .theme-toggle:active::before { 1641 | width: 100%; 1642 | height: 100%; 1643 | } 1644 | 1645 | .theme-icon { 1646 | width: 24px; 1647 | height: 24px; 1648 | transition: all var(--transition-fast); 1649 | position: relative; 1650 | z-index: 1; 1651 | } 1652 | 1653 | .theme-toggle:hover .theme-icon { 1654 | transform: rotate(15deg) scale(1.1); 1655 | } 1656 | 1657 | .logo { 1658 | display: flex; 1659 | align-items: center; 1660 | gap: var(--spacing-sm); 1661 | font-family: var(--font-display); 1662 | font-size: 1.8rem; 1663 | font-weight: 700; 1664 | color: var(--text-primary); 1665 | text-decoration: none; 1666 | flex-shrink: 0; 1667 | } 1668 | 1669 | .logo img { 1670 | width: 150px; 1671 | height: 80px; 1672 | object-fit: contain; 1673 | } 1674 | 1675 | /* Favorites Dropdown */ 1676 | .favorites-dropdown { 1677 | position: relative; 1678 | } 1679 | 1680 | .favorites-button { 1681 | display: flex; 1682 | align-items: center; 1683 | gap: var(--spacing-xs); 1684 | padding: var(--spacing-sm) var(--spacing-md); 1685 | background-color: var(--bg-secondary); 1686 | border: 1px solid var(--bg-tertiary); 1687 | border-radius: var(--radius-sm); 1688 | color: var(--text-primary); 1689 | cursor: pointer; 1690 | transition: all var(--transition-fast); 1691 | } 1692 | 1693 | .favorites-button:hover { 1694 | background-color: var(--bg-tertiary); 1695 | } 1696 | 1697 | .favorites-button img { 1698 | width: 16px; 1699 | height: 16px; 1700 | } 1701 | 1702 | .favorites-menu { 1703 | position: absolute; 1704 | top: 100%; 1705 | right: 0; 1706 | margin-top: var(--spacing-xs); 1707 | background-color: var(--bg-secondary); 1708 | border: 1px solid var(--bg-tertiary); 1709 | border-radius: var(--radius-md); 1710 | box-shadow: var(--shadow-lg); 1711 | padding: var(--spacing-md); 1712 | min-width: 280px; 1713 | max-width: 320px; 1714 | z-index: 1000; 1715 | opacity: 0; 1716 | visibility: hidden; 1717 | transform: translateY(-10px); 1718 | transition: all var(--transition-fast); 1719 | } 1720 | 1721 | .favorites-menu.active { 1722 | opacity: 1; 1723 | visibility: visible; 1724 | transform: translateY(0); 1725 | } 1726 | 1727 | .favorites-header h4 { 1728 | font-size: 0.875rem; 1729 | font-weight: 600; 1730 | color: var(--text-secondary); 1731 | margin-bottom: var(--spacing-md); 1732 | } 1733 | 1734 | .favorites-list { 1735 | max-height: 300px; 1736 | overflow-y: auto; 1737 | } 1738 | 1739 | .favorite-item { 1740 | display: flex; 1741 | justify-content: space-between; 1742 | align-items: center; 1743 | padding: var(--spacing-sm); 1744 | border-radius: var(--radius-sm); 1745 | cursor: pointer; 1746 | transition: background-color var(--transition-fast); 1747 | margin-bottom: var(--spacing-xs); 1748 | } 1749 | 1750 | .favorite-item:hover { 1751 | background-color: var(--bg-tertiary); 1752 | } 1753 | 1754 | .favorite-info { 1755 | flex: 1; 1756 | } 1757 | 1758 | .favorite-name { 1759 | font-weight: 600; 1760 | color: var(--text-primary); 1761 | margin-bottom: 2px; 1762 | } 1763 | 1764 | .favorite-country { 1765 | font-size: 0.875rem; 1766 | color: var(--text-secondary); 1767 | } 1768 | 1769 | .favorite-remove { 1770 | background: none; 1771 | border: none; 1772 | color: var(--text-tertiary); 1773 | cursor: pointer; 1774 | padding: var(--spacing-xs); 1775 | border-radius: var(--radius-sm); 1776 | transition: all var(--transition-fast); 1777 | } 1778 | 1779 | .favorite-remove:hover { 1780 | color: var(--text-primary); 1781 | background-color: var(--bg-tertiary); 1782 | } 1783 | 1784 | .no-favorites { 1785 | text-align: center; 1786 | color: var(--text-secondary); 1787 | padding: var(--spacing-lg); 1788 | } 1789 | 1790 | .no-favorites .text-small { 1791 | font-size: 0.875rem; 1792 | margin-top: var(--spacing-xs); 1793 | } 1794 | 1795 | /* Units Dropdown */ 1796 | .units-dropdown { 1797 | position: relative; 1798 | } 1799 | 1800 | .units-button { 1801 | display: flex; 1802 | align-items: center; 1803 | gap: var(--spacing-xs); 1804 | padding: var(--spacing-sm) var(--spacing-md); 1805 | background-color: var(--bg-secondary); 1806 | border: 1px solid var(--bg-tertiary); 1807 | border-radius: var(--radius-sm); 1808 | color: var(--text-primary); 1809 | cursor: pointer; 1810 | transition: all var(--transition-fast); 1811 | } 1812 | 1813 | .units-button:hover { 1814 | background-color: var(--bg-tertiary); 1815 | } 1816 | 1817 | .units-button img { 1818 | width: 16px; 1819 | height: 16px; 1820 | } 1821 | 1822 | .units-menu { 1823 | position: absolute; 1824 | top: 100%; 1825 | right: 0; 1826 | margin-top: var(--spacing-xs); 1827 | background-color: var(--bg-secondary); 1828 | border: 1px solid var(--bg-tertiary); 1829 | border-radius: var(--radius-md); 1830 | box-shadow: var(--shadow-lg); 1831 | padding: var(--spacing-md); 1832 | min-width: 280px; 1833 | z-index: 1000; 1834 | opacity: 0; 1835 | visibility: hidden; 1836 | transform: translateY(-10px); 1837 | transition: all var(--transition-fast); 1838 | } 1839 | 1840 | .units-menu.active { 1841 | opacity: 1; 1842 | visibility: visible; 1843 | transform: translateY(0); 1844 | } 1845 | 1846 | .units-section { 1847 | margin-bottom: var(--spacing-md); 1848 | } 1849 | 1850 | .units-section:last-child { 1851 | margin-bottom: 0; 1852 | } 1853 | 1854 | .units-section h4 { 1855 | font-size: 0.875rem; 1856 | font-weight: 600; 1857 | color: var(--text-secondary); 1858 | margin-bottom: var(--spacing-xs); 1859 | } 1860 | 1861 | .units-options { 1862 | display: flex; 1863 | flex-direction: column; 1864 | gap: var(--spacing-xs); 1865 | } 1866 | 1867 | .unit-option { 1868 | display: flex; 1869 | align-items: center; 1870 | gap: var(--spacing-xs); 1871 | padding: var(--spacing-xs); 1872 | border-radius: var(--radius-sm); 1873 | cursor: pointer; 1874 | transition: background-color var(--transition-fast); 1875 | } 1876 | 1877 | .unit-option:hover { 1878 | background-color: var(--bg-tertiary); 1879 | } 1880 | 1881 | .unit-option input[type="radio"] { 1882 | margin: 0; 1883 | } 1884 | 1885 | /* Main Title */ 1886 | .main-title { 1887 | text-align: center; 1888 | font-size: 3.5rem; 1889 | font-weight: 700; 1890 | margin-bottom: var(--spacing-xl); 1891 | color: var(--text-primary); 1892 | font-family: var(--font-display); 1893 | line-height: 1.2; 1894 | letter-spacing: -0.02em; 1895 | } 1896 | 1897 | /* Search Section */ 1898 | .search-section { 1899 | margin-bottom: var(--spacing-xl); 1900 | } 1901 | 1902 | .search-container { 1903 | position: relative; 1904 | max-width: 700px; 1905 | margin: 0 auto; 1906 | display: flex; 1907 | gap: var(--spacing-md); 1908 | align-items: center; 1909 | } 1910 | 1911 | .search-input-wrapper { 1912 | position: relative; 1913 | display: flex; 1914 | align-items: center; 1915 | flex: 1; 1916 | } 1917 | 1918 | .search-input { 1919 | padding-left: 3.5rem; 1920 | padding-right: var(--spacing-lg); 1921 | font-size: 1.125rem; 1922 | height: 64px; 1923 | border-radius: var(--radius-lg); 1924 | font-weight: 500; 1925 | width: 100%; 1926 | } 1927 | 1928 | .search-icon { 1929 | position: absolute; 1930 | left: var(--spacing-lg); 1931 | width: 24px; 1932 | height: 24px; 1933 | opacity: 0.6; 1934 | z-index: 2; 1935 | } 1936 | 1937 | .search-button { 1938 | height: 64px; 1939 | padding: 0 var(--spacing-xl); 1940 | border-radius: var(--radius-lg); 1941 | font-size: 1rem; 1942 | font-weight: 600; 1943 | white-space: nowrap; 1944 | } 1945 | 1946 | /* Search Results */ 1947 | .search-results { 1948 | position: absolute; 1949 | top: 100%; 1950 | left: 0; 1951 | right: 0; 1952 | background-color: var(--bg-secondary); 1953 | border: 1px solid var(--bg-tertiary); 1954 | border-radius: var(--radius-md); 1955 | box-shadow: var(--shadow-lg); 1956 | margin-top: var(--spacing-xs); 1957 | max-height: 300px; 1958 | overflow-y: auto; 1959 | z-index: 1000; 1960 | opacity: 0; 1961 | visibility: hidden; 1962 | transform: translateY(-10px); 1963 | transition: all var(--transition-fast); 1964 | } 1965 | 1966 | .search-results.active { 1967 | opacity: 1; 1968 | visibility: visible; 1969 | transform: translateY(0); 1970 | } 1971 | 1972 | .search-result-item { 1973 | padding: var(--spacing-md); 1974 | border-bottom: 1px solid var(--bg-tertiary); 1975 | cursor: pointer; 1976 | transition: background-color var(--transition-fast); 1977 | } 1978 | 1979 | .search-result-item:last-child { 1980 | border-bottom: none; 1981 | } 1982 | 1983 | .search-result-item:hover, 1984 | .search-result-item.active { 1985 | background-color: var(--bg-tertiary); 1986 | } 1987 | 1988 | .search-result-name { 1989 | font-weight: 600; 1990 | color: var(--text-primary); 1991 | margin-bottom: 2px; 1992 | } 1993 | 1994 | .search-result-details { 1995 | font-size: 0.875rem; 1996 | color: var(--text-secondary); 1997 | } 1998 | 1999 | /* Weather Data Layout */ 2000 | .weather-data { 2001 | display: grid; 2002 | grid-template-columns: 1fr; 2003 | gap: var(--spacing-lg); 2004 | max-width: 100%; 2005 | margin: 0 auto; 2006 | padding: 0 var(--spacing-md); 2007 | align-items: stretch; 2008 | width: 100%; 2009 | box-sizing: border-box; 2010 | } 2011 | 2012 | @media (min-width: 1024px) { 2013 | .weather-data { 2014 | grid-template-columns: 1fr 400px; 2015 | gap: var(--spacing-xs); 2016 | max-width: 1500px; 2017 | padding: 0 var(--spacing-lg); 2018 | } 2019 | } 2020 | 2021 | .weather-left-column { 2022 | display: flex; 2023 | flex-direction: column; 2024 | gap: var(--spacing-lg); 2025 | width: 100%; 2026 | } 2027 | 2028 | 2029 | 2030 | /* Current Weather Card */ 2031 | .current-weather { 2032 | background: url('./assets/images/bg-today-large.svg') center/cover; 2033 | color: var(--neutral-0); 2034 | position: relative; 2035 | overflow: hidden; 2036 | width: 100%; 2037 | min-height: 200px; 2038 | border-radius: var(--radius-lg); 2039 | box-shadow: var(--shadow-lg); 2040 | box-sizing: border-box; 2041 | } 2042 | 2043 | .current-weather-content { 2044 | position: relative; 2045 | z-index: 1; 2046 | height: 100%; 2047 | display: flex; 2048 | flex-direction: column; 2049 | justify-content: center; 2050 | padding: var(--spacing-lg); 2051 | } 2052 | 2053 | .current-weather-header { 2054 | display: flex; 2055 | justify-content: space-between; 2056 | align-items: flex-start; 2057 | margin-bottom: var(--spacing-md); 2058 | position: absolute; 2059 | top: var(--spacing-sm); 2060 | right: var(--spacing-md); 2061 | left: var(--spacing-lg); 2062 | z-index: 2; 2063 | } 2064 | 2065 | .location-info { 2066 | flex: 1; 2067 | margin-top: var(--spacing-xl); 2068 | } 2069 | 2070 | .current-location { 2071 | font-size: 1.5rem; 2072 | font-weight: 600; 2073 | margin-bottom: var(--spacing-xs); 2074 | line-height: 1.2; 2075 | } 2076 | 2077 | .current-date { 2078 | color: rgba(255, 255, 255, 0.8); 2079 | font-size: 0.9rem; 2080 | line-height: 1.4; 2081 | } 2082 | 2083 | .favorite-button { 2084 | background: rgba(255, 255, 255, 0.1); 2085 | border: 1px solid rgba(255, 255, 255, 0.2); 2086 | border-radius: var(--radius-md); 2087 | padding: var(--spacing-sm); 2088 | cursor: pointer; 2089 | transition: all var(--transition-fast); 2090 | backdrop-filter: blur(10px); 2091 | width: 48px; 2092 | height: 48px; 2093 | display: flex; 2094 | align-items: center; 2095 | justify-content: center; 2096 | } 2097 | 2098 | .favorite-button:hover { 2099 | background: rgba(255, 255, 255, 0.2); 2100 | border-color: rgba(255, 255, 255, 0.3); 2101 | transform: translateY(-1px); 2102 | } 2103 | 2104 | .favorite-button.active { 2105 | background: var(--orange-500); 2106 | border-color: var(--orange-500); 2107 | } 2108 | 2109 | .favorite-icon { 2110 | width: 24px; 2111 | height: 24px; 2112 | filter: brightness(0) invert(1); 2113 | } 2114 | 2115 | .current-temp-section { 2116 | display: flex; 2117 | align-items: center; 2118 | justify-content: flex-end; 2119 | margin: var(--spacing-lg) 0; 2120 | position: relative; 2121 | gap: var(--spacing-md); 2122 | } 2123 | 2124 | .current-temp { 2125 | font-size: 5rem; 2126 | font-weight: 700; 2127 | line-height: 1; 2128 | text-align: right; 2129 | } 2130 | 2131 | .current-weather-icon { 2132 | width: 80px; 2133 | height: 80px; 2134 | order: -1; 2135 | } 2136 | 2137 | .current-description { 2138 | font-size: 1.1rem; 2139 | color: rgba(255, 255, 255, 0.9); 2140 | text-align: center; 2141 | margin-top: var(--spacing-md); 2142 | font-weight: 500; 2143 | } 2144 | 2145 | /* Weather Metrics */ 2146 | .weather-metrics { 2147 | display: grid; 2148 | grid-template-columns: repeat(4, 1fr); 2149 | gap: var(--spacing-sm); 2150 | margin-bottom: var(--spacing-lg); 2151 | } 2152 | 2153 | .metric-card { 2154 | text-align: center; 2155 | padding: var(--spacing-sm); 2156 | background-color: hsl(243, 27%, 20%); 2157 | border-radius: var(--radius-md); 2158 | box-shadow: var(--shadow-md); 2159 | transition: all var(--transition-normal); 2160 | border: 1px solid hsl(240, 20%, 25%); 2161 | min-height: 80px; 2162 | display: flex; 2163 | flex-direction: column; 2164 | justify-content: center; 2165 | align-items: center; 2166 | } 2167 | 2168 | .metric-card:hover { 2169 | transform: translateY(-4px) scale(1.02); 2170 | box-shadow: var(--shadow-lg); 2171 | } 2172 | 2173 | .metric-label { 2174 | font-size: 0.875rem; 2175 | color: var(--text-secondary); 2176 | margin-bottom: var(--spacing-sm); 2177 | text-transform: uppercase; 2178 | letter-spacing: 0.5px; 2179 | font-weight: 500; 2180 | } 2181 | 2182 | .metric-value { 2183 | font-size: 1.75rem; 2184 | font-weight: 700; 2185 | color: var(--text-primary); 2186 | line-height: 1.2; 2187 | } 2188 | 2189 | /* Sun Times Card */ 2190 | .sun-times .metric-value { 2191 | display: none; 2192 | } 2193 | 2194 | .sun-times-content { 2195 | display: flex; 2196 | flex-direction: column; 2197 | gap: var(--spacing-xs); 2198 | } 2199 | 2200 | .sun-time { 2201 | display: flex; 2202 | justify-content: space-between; 2203 | align-items: center; 2204 | } 2205 | 2206 | .sun-label { 2207 | font-size: 0.875rem; 2208 | color: var(--text-secondary); 2209 | } 2210 | 2211 | .sun-value { 2212 | font-size: 1rem; 2213 | font-weight: 600; 2214 | color: var(--text-primary); 2215 | } 2216 | 2217 | /* Accessibility Features */ 2218 | @media (prefers-reduced-motion: reduce) { 2219 | *, 2220 | *::before, 2221 | *::after { 2222 | animation-duration: 0.01ms !important; 2223 | animation-iteration-count: 1 !important; 2224 | transition-duration: 0.01ms !important; 2225 | } 2226 | 2227 | .weather-particles { 2228 | display: none; 2229 | } 2230 | } 2231 | 2232 | /* High contrast mode support */ 2233 | @media (prefers-contrast: high) { 2234 | :root { 2235 | --shadow-sm: 0 2px 4px 0 rgb(0 0 0 / 0.3); 2236 | --shadow-md: 0 4px 8px 0 rgb(0 0 0 / 0.3); 2237 | --shadow-lg: 0 8px 16px 0 rgb(0 0 0 / 0.3); 2238 | } 2239 | 2240 | .card { 2241 | border: 2px solid var(--text-tertiary); 2242 | } 2243 | 2244 | .btn { 2245 | border: 2px solid currentColor; 2246 | } 2247 | } 2248 | 2249 | /* Focus visible for better keyboard navigation */ 2250 | .btn:focus-visible, 2251 | .input:focus-visible, 2252 | .units-button:focus-visible, 2253 | .favorites-button:focus-visible, 2254 | .theme-toggle:focus-visible { 2255 | outline: 3px solid var(--blue-500); 2256 | outline-offset: 2px; 2257 | } 2258 | 2259 | /* Skip link for screen readers */ 2260 | .skip-link { 2261 | position: absolute; 2262 | top: -40px; 2263 | left: 6px; 2264 | background: var(--blue-500); 2265 | color: var(--neutral-0); 2266 | padding: 8px; 2267 | text-decoration: none; 2268 | border-radius: 4px; 2269 | z-index: 9999; 2270 | } 2271 | 2272 | .skip-link:focus { 2273 | top: 6px; 2274 | } 2275 | 2276 | 2277 | 2278 | 2279 | 2280 | 2281 | 2282 | 2283 | 2284 | /* Forecast Sections */ 2285 | .section-title { 2286 | font-size: 1.1rem; 2287 | font-weight: 600; 2288 | margin-bottom: var(--spacing-md); 2289 | color: var(--text-primary); 2290 | } 2291 | 2292 | /* Daily Forecast */ 2293 | .daily-forecast { 2294 | padding: var(--spacing-md); 2295 | min-height: 200px; 2296 | box-shadow: var(--shadow-md); 2297 | margin-bottom: var(--spacing-xl); 2298 | } 2299 | 2300 | .forecast-days { 2301 | display: flex; 2302 | gap: var(--spacing-sm); 2303 | overflow-x: auto; 2304 | padding-bottom: var(--spacing-sm); 2305 | scrollbar-width: thin; 2306 | } 2307 | 2308 | .forecast-days::-webkit-scrollbar { 2309 | height: 6px; 2310 | } 2311 | 2312 | .forecast-days::-webkit-scrollbar-track { 2313 | background: var(--bg-tertiary); 2314 | border-radius: 3px; 2315 | } 2316 | 2317 | .forecast-days::-webkit-scrollbar-thumb { 2318 | background: var(--text-tertiary); 2319 | border-radius: 3px; 2320 | } 2321 | 2322 | .forecast-day { 2323 | display: flex; 2324 | flex-direction: column; 2325 | align-items: center; 2326 | padding: var(--spacing-sm); 2327 | border-radius: var(--radius-md); 2328 | background-color: hsl(243, 27%, 20%); 2329 | transition: all var(--transition-normal); 2330 | cursor: pointer; 2331 | min-width: 110px; 2332 | min-height: 120px; 2333 | flex-shrink: 0; 2334 | box-shadow: var(--shadow-sm); 2335 | border: 1px solid hsl(240, 20%, 25%); 2336 | } 2337 | 2338 | .forecast-day:hover { 2339 | background-color: var(--bg-tertiary); 2340 | transform: translateY(-4px) scale(1.02); 2341 | box-shadow: var(--shadow-lg); 2342 | } 2343 | 2344 | .day-name { 2345 | font-weight: 600; 2346 | color: var(--text-primary); 2347 | margin-bottom: var(--spacing-md); 2348 | font-size: 1rem; 2349 | text-align: center; 2350 | } 2351 | 2352 | .day-icon { 2353 | width: 50px; 2354 | height: 50px; 2355 | margin-bottom: var(--spacing-md); 2356 | } 2357 | 2358 | .day-temps { 2359 | display: flex; 2360 | flex-direction: column; 2361 | align-items: center; 2362 | gap: var(--spacing-xs); 2363 | margin-top: auto; 2364 | width: 100%; 2365 | } 2366 | 2367 | .temp-high { 2368 | font-weight: 600; 2369 | color: var(--text-primary); 2370 | font-size: 1.1rem; 2371 | line-height: 1.2; 2372 | } 2373 | 2374 | .temp-low { 2375 | color: var(--text-secondary); 2376 | font-size: 0.9rem; 2377 | font-weight: 500; 2378 | line-height: 1.2; 2379 | } 2380 | 2381 | /* Hourly Forecast */ 2382 | .hourly-forecast { 2383 | background-color: hsl(240, 25%, 16%); 2384 | border-radius: var(--radius-lg); 2385 | padding: var(--spacing-lg); 2386 | height: 100%; 2387 | display: flex; 2388 | flex-direction: column; 2389 | box-shadow: var(--shadow-lg); 2390 | border: 1px solid hsl(240, 20%, 25%); 2391 | overflow: hidden; 2392 | } 2393 | 2394 | .hourly-header { 2395 | display: flex; 2396 | justify-content: space-between; 2397 | align-items: center; 2398 | margin-bottom: var(--spacing-lg); 2399 | padding-bottom: var(--spacing-sm); 2400 | border-bottom: 1px solid var(--bg-tertiary); 2401 | } 2402 | 2403 | .section-title { 2404 | font-size: 1.1rem; 2405 | font-weight: 600; 2406 | color: var(--text-primary); 2407 | margin: 0; 2408 | } 2409 | 2410 | .day-selector { 2411 | padding: var(--spacing-sm) var(--spacing-md); 2412 | border: 1px solid var(--bg-tertiary); 2413 | border-radius: var(--radius-md); 2414 | background-color: var(--bg-primary); 2415 | color: var(--text-primary); 2416 | font-family: var(--font-primary); 2417 | cursor: pointer; 2418 | font-size: 1rem; 2419 | font-weight: 500; 2420 | transition: all var(--transition-fast); 2421 | } 2422 | 2423 | .day-selector:focus { 2424 | outline: none; 2425 | border-color: var(--blue-500); 2426 | box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); 2427 | } 2428 | 2429 | .day-selector:hover { 2430 | border-color: var(--blue-500); 2431 | } 2432 | 2433 | .forecast-hours { 2434 | display: flex; 2435 | flex-direction: column; 2436 | gap: var(--spacing-xs); 2437 | flex: 1; 2438 | overflow-y: auto; 2439 | scrollbar-width: thin; 2440 | height: 0; 2441 | min-height: 100%; 2442 | } 2443 | 2444 | .forecast-hours::-webkit-scrollbar { 2445 | width: 6px; 2446 | } 2447 | 2448 | .forecast-hours::-webkit-scrollbar-track { 2449 | background: var(--bg-tertiary); 2450 | border-radius: 3px; 2451 | } 2452 | 2453 | .forecast-hours::-webkit-scrollbar-thumb { 2454 | background: var(--text-tertiary); 2455 | border-radius: 3px; 2456 | } 2457 | 2458 | .forecast-hour { 2459 | display: flex; 2460 | align-items: center; 2461 | justify-content: space-between; 2462 | padding: var(--spacing-md) var(--spacing-lg); 2463 | border-radius: var(--radius-md); 2464 | transition: all var(--transition-fast); 2465 | border: 1px solid var(--bg-tertiary); 2466 | min-height: 65px; 2467 | background-color: hsl(243, 27%, 20%); 2468 | } 2469 | 2470 | .forecast-hour:hover { 2471 | background-color: var(--bg-tertiary); 2472 | border-color: var(--text-tertiary); 2473 | transform: translateY(-2px); 2474 | box-shadow: var(--shadow-sm); 2475 | } 2476 | 2477 | .hour-time { 2478 | font-weight: 600; 2479 | color: var(--text-primary); 2480 | min-width: 60px; 2481 | font-size: 1rem; 2482 | text-align: left; 2483 | } 2484 | 2485 | .hour-icon { 2486 | width: 32px; 2487 | height: 32px; 2488 | margin: 0 var(--spacing-md); 2489 | flex-shrink: 0; 2490 | } 2491 | 2492 | .hour-temp { 2493 | font-weight: 700; 2494 | color: var(--text-primary); 2495 | min-width: 50px; 2496 | text-align: right; 2497 | font-size: 1.1rem; 2498 | } 2499 | 2500 | /* Loading and Error States */ 2501 | .loading-state, 2502 | .error-state { 2503 | text-align: center; 2504 | padding: var(--spacing-2xl); 2505 | color: var(--text-secondary); 2506 | animation: fadeIn var(--transition-normal) ease-in-out; 2507 | } 2508 | 2509 | .loading-icon, 2510 | .error-icon { 2511 | width: 64px; 2512 | height: 64px; 2513 | margin-bottom: var(--spacing-lg); 2514 | opacity: 0.6; 2515 | } 2516 | 2517 | .loading-icon { 2518 | animation: spin 1s linear infinite; 2519 | } 2520 | 2521 | @keyframes spin { 2522 | from { 2523 | transform: rotate(0deg); 2524 | } 2525 | to { 2526 | transform: rotate(360deg); 2527 | } 2528 | } 2529 | 2530 | .error-state h3 { 2531 | color: var(--text-primary); 2532 | margin-bottom: var(--spacing-sm); 2533 | } 2534 | 2535 | .error-state p { 2536 | margin-bottom: var(--spacing-lg); 2537 | } 2538 | 2539 | /* Smooth transitions for state changes */ 2540 | .weather-content > * { 2541 | transition: opacity var(--transition-normal) ease-in-out; 2542 | } 2543 | 2544 | .weather-content .hidden { 2545 | opacity: 0; 2546 | pointer-events: none; 2547 | } 2548 | 2549 | /* Loading skeleton for cards */ 2550 | .loading .metric-card { 2551 | background: linear-gradient(90deg, var(--bg-secondary) 25%, var(--bg-tertiary) 50%, var(--bg-secondary) 75%); 2552 | background-size: 200% 100%; 2553 | animation: shimmer 1.5s infinite; 2554 | } 2555 | 2556 | @keyframes shimmer { 2557 | 0% { 2558 | background-position: -200% 0; 2559 | } 2560 | 100% { 2561 | background-position: 200% 0; 2562 | } 2563 | } 2564 | 2565 | /* Mobile Search Results Improvements */ 2566 | @media (max-width: 767px) { 2567 | .search-results { 2568 | position: absolute; 2569 | top: 100%; 2570 | left: 0; 2571 | right: 0; 2572 | background: var(--bg-secondary); 2573 | border: 1px solid var(--border-color); 2574 | border-radius: var(--radius-lg); 2575 | box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); 2576 | max-height: 60vh; 2577 | overflow-y: auto; 2578 | z-index: 100; 2579 | margin-top: var(--spacing-xs); 2580 | } 2581 | 2582 | .search-result-item { 2583 | padding: var(--spacing-md) var(--spacing-lg); 2584 | border-bottom: 1px solid var(--border-color); 2585 | cursor: pointer; 2586 | transition: background-color 0.2s ease; 2587 | } 2588 | 2589 | .search-result-item:hover, 2590 | .search-result-item:focus { 2591 | background: var(--bg-tertiary); 2592 | } 2593 | 2594 | .search-result-item:last-child { 2595 | border-bottom: none; 2596 | } 2597 | } 2598 | 2599 | /* Additional Responsive Improvements */ 2600 | @media (max-width: 768px) { 2601 | /* Search results responsive */ 2602 | .search-results { 2603 | position: fixed; 2604 | top: 50%; 2605 | left: 50%; 2606 | transform: translate(-50%, -50%); 2607 | width: 90vw; 2608 | max-width: 400px; 2609 | max-height: 60vh; 2610 | z-index: 1001; 2611 | border-radius: var(--radius-lg); 2612 | } 2613 | 2614 | .search-result-item { 2615 | padding: var(--spacing-md); 2616 | min-height: 48px; 2617 | display: flex; 2618 | align-items: center; 2619 | } 2620 | 2621 | /* Hourly forecast responsive */ 2622 | .hourly-header { 2623 | flex-direction: column; 2624 | gap: var(--spacing-md); 2625 | align-items: stretch; 2626 | } 2627 | 2628 | .day-selector { 2629 | width: 100%; 2630 | min-height: 48px; 2631 | padding: var(--spacing-sm); 2632 | font-size: 1rem; 2633 | } 2634 | 2635 | /* Daily forecast responsive */ 2636 | .daily-forecast { 2637 | overflow-x: auto; 2638 | padding-bottom: var(--spacing-sm); 2639 | scrollbar-width: thin; 2640 | } 2641 | 2642 | .daily-forecast::-webkit-scrollbar { 2643 | height: 4px; 2644 | } 2645 | 2646 | .daily-forecast::-webkit-scrollbar-track { 2647 | background: var(--bg-tertiary); 2648 | border-radius: 2px; 2649 | } 2650 | 2651 | .daily-forecast::-webkit-scrollbar-thumb { 2652 | background: var(--text-tertiary); 2653 | border-radius: 2px; 2654 | } 2655 | 2656 | .forecast-day { 2657 | min-width: 100px; 2658 | flex-shrink: 0; 2659 | padding: var(--spacing-md); 2660 | } 2661 | 2662 | .forecast-hour { 2663 | padding: var(--spacing-sm); 2664 | } 2665 | 2666 | .day-icon, 2667 | .hour-icon { 2668 | width: 40px; 2669 | height: 40px; 2670 | } 2671 | 2672 | /* Mobile-specific improvements */ 2673 | .app { 2674 | padding: var(--spacing-md) 0; 2675 | } 2676 | 2677 | .weather-content { 2678 | padding: 0; 2679 | } 2680 | 2681 | /* Better spacing for mobile cards */ 2682 | .card:not(:last-child) { 2683 | margin-bottom: var(--spacing-lg); 2684 | } 2685 | 2686 | /* Improved touch targets */ 2687 | .favorite-button { 2688 | min-width: 48px; 2689 | min-height: 48px; 2690 | padding: var(--spacing-sm); 2691 | } 2692 | 2693 | /* Mobile search improvements */ 2694 | .search-input-wrapper { 2695 | position: relative; 2696 | width: 100%; 2697 | } 2698 | 2699 | .search-icon { 2700 | left: var(--spacing-md); 2701 | z-index: 2; 2702 | } 2703 | 2704 | /* Mobile weather metrics improvements */ 2705 | .metric-label { 2706 | font-size: 0.8rem; 2707 | font-weight: 500; 2708 | } 2709 | 2710 | .metric-value { 2711 | font-size: 1.25rem; 2712 | font-weight: 700; 2713 | } 2714 | } 2715 | 2716 | /* Landscape orientation adjustments */ 2717 | @media (max-height: 600px) and (orientation: landscape) { 2718 | .weather-data { 2719 | gap: var(--spacing-sm); 2720 | } 2721 | 2722 | .card { 2723 | padding: var(--spacing-sm); 2724 | } 2725 | 2726 | .main-title { 2727 | font-size: 2rem; 2728 | margin-bottom: var(--spacing-lg); 2729 | } 2730 | 2731 | .forecast-hours { 2732 | max-height: 200px; 2733 | } 2734 | 2735 | .units-menu, 2736 | .favorites-menu { 2737 | max-height: 50vh; 2738 | } 2739 | 2740 | /* Attribution responsive styling */ 2741 | .attribution { 2742 | margin: var(--spacing-xl) var(--spacing-md) var(--spacing-lg); 2743 | max-width: none; 2744 | } 2745 | 2746 | .attribution-content { 2747 | padding: var(--spacing-lg) var(--spacing-md); 2748 | } 2749 | 2750 | .attribution-text { 2751 | font-size: 0.85rem; 2752 | gap: 0.375rem; 2753 | text-align: center; 2754 | } 2755 | 2756 | .attribution-link { 2757 | padding: 0.5rem 1rem; 2758 | font-size: 0.9rem; 2759 | } 2760 | 2761 | .attribution-separator { 2762 | margin: 0 var(--spacing-xs); 2763 | } 2764 | } 2765 | 2766 | /* Additional Mobile Responsiveness Enhancements */ 2767 | 2768 | /* Touch and gesture improvements */ 2769 | @media (max-width: 768px) { 2770 | /* Smooth scrolling for all mobile devices */ 2771 | * { 2772 | -webkit-overflow-scrolling: touch; 2773 | } 2774 | 2775 | /* Better touch feedback */ 2776 | .btn:active, 2777 | .forecast-day:active, 2778 | .forecast-hour:active { 2779 | transform: scale(0.98); 2780 | transition: transform 0.1s ease; 2781 | } 2782 | 2783 | /* Improved focus states for mobile */ 2784 | .btn:focus-visible, 2785 | .input:focus-visible, 2786 | .units-button:focus-visible, 2787 | .favorites-button:focus-visible, 2788 | .theme-toggle:focus-visible { 2789 | outline: 3px solid var(--blue-500); 2790 | outline-offset: 2px; 2791 | border-radius: var(--radius-md); 2792 | } 2793 | 2794 | /* Better text selection on mobile */ 2795 | .current-location, 2796 | .current-date, 2797 | .current-description { 2798 | -webkit-user-select: none; 2799 | -moz-user-select: none; 2800 | -ms-user-select: none; 2801 | user-select: none; 2802 | } 2803 | 2804 | /* Prevent zoom on input focus (iOS) */ 2805 | input[type="text"], 2806 | input[type="search"], 2807 | select, 2808 | textarea { 2809 | font-size: 16px; 2810 | } 2811 | 2812 | /* Improved card shadows for mobile */ 2813 | .card { 2814 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 2815 | } 2816 | 2817 | .card:hover { 2818 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 2819 | } 2820 | 2821 | /* Better spacing for mobile content */ 2822 | .weather-content { 2823 | padding-bottom: var(--spacing-lg); 2824 | } 2825 | 2826 | /* Mobile-specific animations */ 2827 | .card { 2828 | animation: slideInUp 0.6s ease-out; 2829 | } 2830 | 2831 | .metric-card { 2832 | animation: fadeInScale 0.4s ease-out; 2833 | } 2834 | 2835 | @keyframes fadeInScale { 2836 | from { 2837 | opacity: 0; 2838 | transform: scale(0.9); 2839 | } 2840 | to { 2841 | opacity: 1; 2842 | transform: scale(1); 2843 | } 2844 | } 2845 | 2846 | /* Improved loading states for mobile */ 2847 | .loading-state { 2848 | padding: var(--spacing-xl) var(--spacing-md); 2849 | } 2850 | 2851 | .loading-icon { 2852 | animation: spin 1s linear infinite; 2853 | } 2854 | 2855 | /* Better error handling on mobile */ 2856 | .error-state { 2857 | padding: var(--spacing-xl) var(--spacing-md); 2858 | } 2859 | 2860 | .error-state .btn { 2861 | margin-top: var(--spacing-md); 2862 | } 2863 | 2864 | /* Mobile-specific scroll improvements */ 2865 | .forecast-days, 2866 | .forecast-hours { 2867 | scroll-behavior: smooth; 2868 | } 2869 | 2870 | /* Better mobile typography */ 2871 | .current-location { 2872 | font-size: 1.25rem; 2873 | line-height: 1.3; 2874 | } 2875 | 2876 | .current-date { 2877 | font-size: 0.9rem; 2878 | line-height: 1.4; 2879 | } 2880 | 2881 | /* Mobile search improvements */ 2882 | .search-results { 2883 | border-radius: var(--radius-lg); 2884 | box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); 2885 | } 2886 | 2887 | .search-result-item { 2888 | border-radius: var(--radius-sm); 2889 | margin: var(--spacing-xs); 2890 | } 2891 | 2892 | /* Mobile dropdown improvements */ 2893 | .units-menu, 2894 | .favorites-menu { 2895 | box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); 2896 | backdrop-filter: blur(10px); 2897 | } 2898 | 2899 | /* Mobile weather icon improvements */ 2900 | .current-weather-icon, 2901 | .day-icon, 2902 | .hour-icon { 2903 | image-rendering: -webkit-optimize-contrast; 2904 | image-rendering: crisp-edges; 2905 | } 2906 | 2907 | /* Mobile metric card improvements */ 2908 | .metric-card { 2909 | transition: all 0.2s ease; 2910 | } 2911 | 2912 | .metric-card:hover { 2913 | transform: translateY(-2px); 2914 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); 2915 | } 2916 | 2917 | /* Mobile forecast improvements */ 2918 | .forecast-day { 2919 | transition: all 0.2s ease; 2920 | } 2921 | 2922 | .forecast-day:hover { 2923 | transform: translateY(-2px); 2924 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); 2925 | } 2926 | 2927 | /* Mobile hourly forecast improvements */ 2928 | .forecast-hour { 2929 | transition: background-color 0.2s ease; 2930 | } 2931 | 2932 | .forecast-hour:hover { 2933 | background-color: var(--bg-tertiary); 2934 | border-radius: var(--radius-sm); 2935 | } 2936 | } 2937 | 2938 | /* Very small mobile devices (320px and below) */ 2939 | @media (max-width: 320px) { 2940 | .container { 2941 | padding: 0 var(--spacing-xs); 2942 | } 2943 | 2944 | .main-title { 2945 | font-size: 1.25rem; 2946 | line-height: 1.2; 2947 | } 2948 | 2949 | .current-temp { 2950 | font-size: 2.5rem; 2951 | } 2952 | 2953 | .current-weather-icon { 2954 | width: 60px; 2955 | height: 60px; 2956 | } 2957 | 2958 | .forecast-day { 2959 | min-width: 80px; 2960 | padding: var(--spacing-xs); 2961 | } 2962 | 2963 | .day-icon { 2964 | width: 30px; 2965 | height: 30px; 2966 | } 2967 | 2968 | .metric-card { 2969 | padding: var(--spacing-xs); 2970 | min-height: 70px; 2971 | } 2972 | 2973 | .metric-label { 2974 | font-size: 0.7rem; 2975 | } 2976 | 2977 | .metric-value { 2978 | font-size: 1rem; 2979 | } 2980 | 2981 | .btn { 2982 | min-height: 48px; 2983 | font-size: 0.9rem; 2984 | } 2985 | 2986 | .search-input { 2987 | height: 48px; 2988 | font-size: 16px; 2989 | } 2990 | 2991 | .search-button { 2992 | height: 48px; 2993 | } 2994 | } 2995 | 2996 | /* High DPI mobile displays */ 2997 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { 2998 | .current-weather-icon, 2999 | .day-icon, 3000 | .hour-icon { 3001 | image-rendering: -webkit-optimize-contrast; 3002 | image-rendering: crisp-edges; 3003 | } 3004 | } 3005 | 3006 | /* Mobile dark mode improvements */ 3007 | @media (max-width: 768px) and (prefers-color-scheme: dark) { 3008 | .card { 3009 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); 3010 | } 3011 | 3012 | .card:hover { 3013 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); 3014 | } 3015 | } 3016 | --------------------------------------------------------------------------------