portosaurus 3.0.2 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +26 -126
  2. package/bin/portosaurus.mjs +8 -0
  3. package/package.json +6 -3
  4. package/src/assets/img/icon.png +0 -0
  5. package/src/assets/img/{icon.svg → svg/icon.svg} +35 -37
  6. package/src/assets/img/svg/project-blank.svg +140 -0
  7. package/src/assets/sample-resume.pdf +0 -0
  8. package/src/cli/build.mjs +2 -5
  9. package/src/cli/dev.mjs +27 -5
  10. package/src/cli/init.mjs +6 -12
  11. package/src/cli/schema.mjs +211 -0
  12. package/src/core/buildDocuConfig.mjs +305 -188
  13. package/src/core/constants.mjs +7 -1
  14. package/src/template/config.yml +150 -0
  15. package/src/template/notes/welcome.mdx +6 -0
  16. package/src/template/package.json +3 -3
  17. package/src/theme/MDXComponents.js +0 -1
  18. package/src/theme/components/AboutSection/index.js +32 -17
  19. package/src/theme/components/AboutSection/styles.module.css +151 -344
  20. package/src/theme/components/ContactSection/index.js +23 -14
  21. package/src/theme/components/ContactSection/styles.module.css +19 -8
  22. package/src/theme/components/ExperienceSection/index.js +12 -5
  23. package/src/theme/components/HeroSection/index.js +4 -3
  24. package/src/theme/components/HeroSection/styles.module.css +17 -16
  25. package/src/theme/components/NavArrow/index.js +114 -0
  26. package/src/theme/components/NavArrow/styles.module.css +107 -0
  27. package/src/theme/components/NoteIndex/index.js +66 -95
  28. package/src/theme/components/NoteIndex/styles.module.css +85 -89
  29. package/src/theme/components/Preview/components/FeedbackStates.js +3 -1
  30. package/src/theme/components/Preview/components/PreviewContent.js +91 -0
  31. package/src/theme/components/Preview/components/PreviewHeader.js +41 -33
  32. package/src/theme/components/Preview/components/Triggers/Pv.js +129 -72
  33. package/src/theme/components/Preview/components/ViewerWindow.js +198 -234
  34. package/src/theme/components/Preview/hooks/useAdaptiveSizing.js +115 -0
  35. package/src/theme/components/Preview/hooks/useDeepLinkHash.js +18 -23
  36. package/src/theme/components/Preview/hooks/useDockLayout.js +48 -8
  37. package/src/theme/components/Preview/hooks/useTouchZoom.js +118 -0
  38. package/src/theme/components/Preview/renderers/CodeRenderer.js +64 -25
  39. package/src/theme/components/Preview/state/index.js +70 -17
  40. package/src/theme/components/Preview/styles.module.css +181 -45
  41. package/src/theme/components/Preview/utils/index.js +11 -10
  42. package/src/theme/components/ProjectsSection/index.js +138 -148
  43. package/src/theme/components/ProjectsSection/styles.module.css +178 -112
  44. package/src/theme/components/SocialLinks/index.js +9 -7
  45. package/src/theme/components/Tooltip/index.js +31 -20
  46. package/src/theme/components/Tooltip/styles.module.css +101 -38
  47. package/src/theme/config/iconMappings.js +2 -0
  48. package/src/theme/css/custom.css +72 -0
  49. package/src/theme/hooks/useScrollReveal.js +30 -0
  50. package/src/theme/pages/index.js +7 -27
  51. package/src/theme/pages/notes.js +2 -2
  52. package/src/theme/pages/tasks.js +12 -11
  53. package/src/utils/cliUtils.mjs +23 -51
  54. package/src/utils/configUtils.mjs +95 -84
  55. package/src/utils/systemUtils.mjs +171 -0
  56. package/src/template/config.js +0 -68
  57. package/src/theme/components/ScrollToTop/index.js +0 -95
  58. package/src/theme/components/ScrollToTop/styles.module.css +0 -97
  59. package/src/theme/config/metaTags.js +0 -21
  60. /package/src/template/{.nojekyll → static/.nojekyll} +0 -0
@@ -1,6 +1,8 @@
1
+ import React from "react";
1
2
  import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
2
3
  import { iconMap } from "../../config/iconMappings";
3
4
  import { FaQuestionCircle } from "react-icons/fa";
5
+ import useScrollReveal from "../../hooks/useScrollReveal";
4
6
  import styles from "./styles.module.css";
5
7
 
6
8
  const sortEmail = (links) => {
@@ -22,27 +24,34 @@ const sortEmail = (links) => {
22
24
  });
23
25
  };
24
26
 
25
- export default function ContactSection({ id, className, title, subtitle }) {
27
+ export default function ContactSection({ id, className }) {
26
28
  const { siteConfig } = useDocusaurusContext();
27
29
  const { customFields } = siteConfig;
28
- let socialLinks = customFields.socialLinks.links || [];
30
+ const socialLinksConfig = customFields.socialLinks || {};
31
+
32
+ if (socialLinksConfig.enable === false) return null;
33
+
34
+ let socialLinks = socialLinksConfig.links || [];
35
+
36
+ const displayHeading = socialLinksConfig.heading;
37
+ const displaySubheading = socialLinksConfig.subheading;
38
+
39
+ const [sectionRef, isVisible] = useScrollReveal();
29
40
 
30
41
  socialLinks = sortEmail(socialLinks);
31
42
 
32
43
  return (
33
44
  <div
34
45
  id={id}
35
- className={`${styles.contactSection} ${className || ""}`}
46
+ ref={sectionRef}
47
+ className={`${styles.contactSection} ${isVisible ? "is-visible" : ""} ${className || ""}`}
36
48
  role="region"
37
49
  aria-label="Contact section"
38
50
  >
39
51
  <div className={styles.contactContainer}>
40
52
  <div className={styles.contactHeader}>
41
- <h2 className={styles.contactTitle}>{title || "Get In Touch"}</h2>
42
- <p className={styles.contactSubtitle}>
43
- {subtitle ||
44
- "Feel free to reach out for collaborations, questions, or just to say hello!"}
45
- </p>
53
+ <h2 className={styles.contactTitle}>{displayHeading}</h2>
54
+ <p className={styles.contactSubtitle}>{displaySubheading}</p>
46
55
  </div>
47
56
 
48
57
  {/* SocialCard */}
@@ -53,13 +62,13 @@ export default function ContactSection({ id, className, title, subtitle }) {
53
62
  aria-label="Social media and contact links"
54
63
  >
55
64
  {socialLinks.map((social, index) => {
56
- const iconData = iconMap[social.icon] || {};
65
+ const iconKey = (social.icon || social.name || "").toLowerCase();
66
+ const iconData = iconMap[iconKey] || {};
57
67
 
58
- const name = social.name || "?";
68
+ const name = social.name;
59
69
  const Icon = iconData.icon || FaQuestionCircle;
60
70
  const iconColor = iconData.color || "var(--ifm-color-primary)";
61
- const description =
62
- name === "?" ? "" : social.desc || `Connect with me on ${name}`;
71
+ const desc = social.desc || `Connect with me on ${name}`;
63
72
  const url = social.url;
64
73
 
65
74
  return (
@@ -73,7 +82,7 @@ export default function ContactSection({ id, className, title, subtitle }) {
73
82
  "--card-index": index,
74
83
  "--icon-hover-color": iconColor,
75
84
  }}
76
- aria-label={`Connect with me on ${name}: ${description}`}
85
+ aria-label={`Connect with me on ${name}: ${desc}`}
77
86
  role="listitem"
78
87
  >
79
88
  {Icon && (
@@ -82,7 +91,7 @@ export default function ContactSection({ id, className, title, subtitle }) {
82
91
  </div>
83
92
  )}
84
93
  <h3 className={styles.socialTitle}>{name}</h3>
85
- <p className={styles.socialDesc}>{description}</p>
94
+ <p className={styles.socialDesc}>{desc}</p>
86
95
  </a>
87
96
  );
88
97
  })}
@@ -1,8 +1,8 @@
1
1
  /* Animations */
2
- @keyframes slideUp {
2
+ @keyframes fadeIn {
3
3
  from {
4
4
  opacity: 0;
5
- transform: translateY(20px);
5
+ transform: translateY(30px);
6
6
  }
7
7
  to {
8
8
  opacity: 1;
@@ -10,10 +10,10 @@
10
10
  }
11
11
  }
12
12
 
13
- @keyframes fadeIn {
13
+ @keyframes slideUp {
14
14
  from {
15
15
  opacity: 0;
16
- transform: translateY(20px);
16
+ transform: translateY(30px);
17
17
  }
18
18
  to {
19
19
  opacity: 1;
@@ -55,17 +55,25 @@
55
55
  font-weight: 600;
56
56
  color: var(--ifm-color-primary);
57
57
  margin-bottom: 0.6rem;
58
- animation: slideUp 0.5s ease-out forwards;
58
+ opacity: 0;
59
59
  position: relative;
60
60
  display: inline-block;
61
61
  }
62
62
 
63
+ :global(.is-visible) .contactTitle {
64
+ animation: slideUp 0.4s ease-out forwards;
65
+ }
66
+
63
67
  .contactSubtitle {
64
68
  font-size: 0.95rem;
65
69
  color: var(--ifm-font-color-tertiary);
66
70
  max-width: 600px;
67
71
  margin: 0.05rem auto 0;
68
- animation: slideUp 0.5s ease-out 0.2s forwards;
72
+ opacity: 0;
73
+ }
74
+
75
+ :global(.is-visible) .contactSubtitle {
76
+ animation: slideUp 0.4s ease-out 0.1s forwards;
69
77
  }
70
78
 
71
79
  /* SocialCard */
@@ -99,8 +107,6 @@
99
107
  transform 0.3s ease,
100
108
  box-shadow 0.3s ease,
101
109
  background-color 0.3s ease;
102
- animation: fadeIn 0.5s ease-out forwards;
103
- animation-delay: calc(0.1s + (var(--card-index, 0) * 0.1s));
104
110
  box-shadow: var(--ifm-global-shadow-lw);
105
111
  text-decoration: none;
106
112
  display: flex;
@@ -114,6 +120,11 @@
114
120
  overflow: hidden;
115
121
  }
116
122
 
123
+ :global(.is-visible) .socialCard {
124
+ animation: fadeIn 0.8s ease-out forwards;
125
+ animation-delay: calc(0.3s + (var(--card-index, 0) * 0.1s));
126
+ }
127
+
117
128
  .socialCard:hover {
118
129
  transform: translateY(-5px);
119
130
  box-shadow: var(--ifm-global-shadow-md);
@@ -1,6 +1,15 @@
1
+ import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
1
2
  import styles from "./styles.module.css";
2
3
 
3
- export default function ExperienceSection({ id, className, title, subtitle }) {
4
+ export default function ExperienceSection({ id, className }) {
5
+ const { siteConfig } = useDocusaurusContext();
6
+ const experience = siteConfig.customFields?.experience || {};
7
+
8
+ if (experience.enable === false) return null;
9
+
10
+ const displayHeading = experience.heading;
11
+ const displaySubheading = experience.subheading;
12
+
4
13
  return (
5
14
  <div
6
15
  id={id}
@@ -10,10 +19,8 @@ export default function ExperienceSection({ id, className, title, subtitle }) {
10
19
  >
11
20
  <div className={styles.experienceContainer}>
12
21
  <div className={styles.experienceHeader}>
13
- <h2 className={styles.experienceTitle}>{title || "Experience"}</h2>
14
- <p className={styles.experienceSubtitle}>
15
- {subtitle || "My professional journey and work experience"}
16
- </p>
22
+ <h2 className={styles.experienceTitle}>{displayHeading}</h2>
23
+ <p className={styles.experienceSubtitle}>{displaySubheading}</p>
17
24
  </div>
18
25
 
19
26
  <div className={styles.noticeWrapper}>
@@ -1,5 +1,6 @@
1
1
  import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
2
2
  import SocialLinks from "../SocialLinks/index.js";
3
+ import { FaChevronDown } from "react-icons/fa";
3
4
  import styles from "./styles.module.css";
4
5
 
5
6
  export default function HeroSection({ id, className }) {
@@ -10,7 +11,7 @@ export default function HeroSection({ id, className }) {
10
11
  const title = customFields.heroSection.title;
11
12
  const subtitle = customFields.heroSection.subtitle;
12
13
  const profession = customFields.heroSection.profession;
13
- const description = customFields.heroSection.description;
14
+ const desc = customFields.heroSection.desc;
14
15
  const profilePic = customFields.heroSection.profilePic;
15
16
  const learnMoreButtonText = customFields.heroSection.learnMoreButtonTxt;
16
17
 
@@ -33,7 +34,7 @@ export default function HeroSection({ id, className }) {
33
34
  <h2 className={styles.profession}>{profession}</h2>
34
35
  <span className={styles.subtitle}>.</span>
35
36
  </div>
36
- <p className={styles.description}>{description}</p>
37
+ <p className={styles.description}>{desc}</p>
37
38
  <div className={styles.actionRow}>
38
39
  <div className={styles.cta}>
39
40
  <a
@@ -44,7 +45,7 @@ export default function HeroSection({ id, className }) {
44
45
  {learnMoreButtonText}
45
46
  </a>
46
47
  </div>
47
- <SocialLinks />
48
+ <SocialLinks links={customFields.heroSection.social} />
48
49
  </div>
49
50
  </div>
50
51
  <div className={styles.rightSection}>
@@ -11,7 +11,7 @@
11
11
  @keyframes slideUp {
12
12
  from {
13
13
  opacity: 0;
14
- transform: translateY(20px);
14
+ transform: translateY(30px);
15
15
  }
16
16
  to {
17
17
  opacity: 1;
@@ -22,7 +22,7 @@
22
22
  @keyframes slideInLeft {
23
23
  from {
24
24
  opacity: 0;
25
- transform: translateX(-30px);
25
+ transform: translateX(-40px);
26
26
  }
27
27
  to {
28
28
  opacity: 1;
@@ -33,7 +33,7 @@
33
33
  @keyframes slideInRight {
34
34
  from {
35
35
  opacity: 0;
36
- transform: translateX(30px);
36
+ transform: translateX(40px);
37
37
  }
38
38
  to {
39
39
  opacity: 1;
@@ -41,10 +41,10 @@
41
41
  }
42
42
  }
43
43
 
44
- @keyframes gentleAppear {
44
+ @keyframes scaleIn {
45
45
  from {
46
46
  opacity: 0;
47
- transform: scale(0.96);
47
+ transform: scale(0.95);
48
48
  }
49
49
  to {
50
50
  opacity: 1;
@@ -54,6 +54,7 @@
54
54
 
55
55
  /* Base layout */
56
56
  .hero {
57
+ position: relative;
57
58
  display: flex;
58
59
  justify-content: center;
59
60
  align-items: center;
@@ -89,7 +90,7 @@
89
90
  color: var(--ifm-font-color-tertiary);
90
91
  font-family: var(--ifm-font-family-base);
91
92
  margin-bottom: 0.2rem;
92
- animation: slideUp 0.6s ease-out 0.4s both;
93
+ animation: slideUp 0.8s ease-out 0.4s both;
93
94
  }
94
95
 
95
96
  .title {
@@ -98,7 +99,7 @@
98
99
  margin: 0.2rem 0;
99
100
  color: var(--ifm-color-primary);
100
101
  font-family: var(--ifm-font-family-base);
101
- animation: slideUp 0.6s ease-out 0.6s both;
102
+ animation: slideUp 0.8s ease-out 0.6s both;
102
103
  transition: all 0.3s ease;
103
104
  position: relative;
104
105
  display: inline-block;
@@ -132,7 +133,7 @@
132
133
  font-family: var(--ifm-font-family-base);
133
134
  margin-left: 0.1rem;
134
135
  margin-right: 0.5rem;
135
- animation: slideUp 0.6s ease-out 0.6s both;
136
+ animation: slideUp 0.8s ease-out 0.6s both;
136
137
  }
137
138
 
138
139
  /* Subtitle section */
@@ -140,7 +141,7 @@
140
141
  display: flex;
141
142
  align-items: baseline;
142
143
  flex-wrap: wrap;
143
- animation: slideUp 0.6s ease-out 0.8s both;
144
+ animation: slideUp 0.8s ease-out 0.8s both;
144
145
  }
145
146
 
146
147
  .subtitle {
@@ -189,7 +190,7 @@
189
190
  max-width: 600px;
190
191
  font-family: var(--ifm-font-family-base);
191
192
  text-align: left;
192
- animation: slideUp 0.6s ease-out 1s both;
193
+ animation: slideUp 0.8s ease-out 1s both;
193
194
  }
194
195
 
195
196
  /* Action row styling */
@@ -197,7 +198,7 @@
197
198
  display: flex;
198
199
  align-items: center;
199
200
  margin-top: 2.5rem;
200
- animation: slideUp 0.6s ease-out 1.2s both;
201
+ animation: slideUp 0.8s ease-out 1.2s both;
201
202
  }
202
203
 
203
204
  .cta {
@@ -255,20 +256,20 @@
255
256
  height: 270px;
256
257
  border-radius: 20%;
257
258
  object-fit: cover;
258
- box-shadow: 0 10px 25px var(--ifm-shadow-color);
259
+ box-shadow: none;
259
260
  margin-top: -30px;
260
261
  margin-right: 2rem;
261
262
  transition:
262
263
  transform 0.5s ease,
263
264
  box-shadow 0.5s ease;
264
- animation: gentleAppear 0.8s ease-out both;
265
+ animation: scaleIn 0.8s ease-out both;
265
266
  will-change: transform;
266
267
  position: relative;
267
268
  }
268
269
 
269
270
  .profilePic:hover {
270
271
  transform: scale(1.04) translateY(-8px);
271
- box-shadow: 0 15px 30px var(--ifm-shadow-color);
272
+ box-shadow: 0 20px 40px var(--ifm-shadow-color);
272
273
  }
273
274
 
274
275
  /* Responsive styles for sections */
@@ -311,7 +312,7 @@
311
312
  margin-top: -20px;
312
313
  margin-bottom: 1.2rem;
313
314
  margin-right: 0;
314
- box-shadow: 0 8px 20px var(--ifm-shadow-color);
315
+ box-shadow: none;
315
316
  }
316
317
 
317
318
  .profilePic:hover {
@@ -418,7 +419,7 @@
418
419
  height: 160px;
419
420
  margin-top: -25px;
420
421
  margin-bottom: 1.5rem;
421
- box-shadow: 0 6px 15px var(--ifm-shadow-color);
422
+ box-shadow: none;
422
423
  }
423
424
 
424
425
  .title {
@@ -0,0 +1,114 @@
1
+ import React, { useState, useEffect, useRef } from "react";
2
+ import { FaChevronDown, FaChevronUp } from "react-icons/fa";
3
+ import Tooltip from "../Tooltip";
4
+ import styles from "./styles.module.css";
5
+
6
+ export default function NavArrow() {
7
+ const [direction, setDirection] = useState("down");
8
+ const [isVisible, setIsVisible] = useState(false);
9
+ const [isScrolling, setIsScrolling] = useState(false);
10
+ const scrollTimeoutRef = useRef(null);
11
+
12
+ const getSections = () => {
13
+ // Dynamically find all main sections with IDs
14
+ const main = document.querySelector("main");
15
+ if (!main) return [];
16
+
17
+ return Array.from(main.querySelectorAll(":scope > [id]"))
18
+ .map((el) => el.id)
19
+ .filter((id) => id !== "nav-arrow"); // Exclude itself if it had an ID
20
+ };
21
+
22
+ useEffect(() => {
23
+ const handleScroll = () => {
24
+ const scrollTop =
25
+ window.pageYOffset || document.documentElement.scrollTop;
26
+ const windowHeight = window.innerHeight;
27
+ const fullHeight = document.documentElement.scrollHeight;
28
+
29
+ // Show after initial scroll
30
+ setIsVisible(scrollTop > 100);
31
+
32
+ // Point UP only if we are at the very bottom
33
+ if (scrollTop + windowHeight >= fullHeight - 50) {
34
+ setDirection("up");
35
+ } else {
36
+ setDirection("down");
37
+ }
38
+
39
+ setIsScrolling(true);
40
+ if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current);
41
+ scrollTimeoutRef.current = setTimeout(() => setIsScrolling(false), 800);
42
+ };
43
+
44
+ window.addEventListener("scroll", handleScroll, { passive: true });
45
+ handleScroll();
46
+
47
+ return () => {
48
+ window.removeEventListener("scroll", handleScroll);
49
+ if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current);
50
+ };
51
+ }, []);
52
+
53
+ const handleClick = () => {
54
+ if (direction === "up") {
55
+ window.scrollTo({ top: 0, behavior: "smooth" });
56
+ } else {
57
+ const sections = getSections();
58
+ const windowTop =
59
+ window.pageYOffset || document.documentElement.scrollTop;
60
+
61
+ let nextSectionId = null;
62
+
63
+ // Find the first section whose top is below the current scroll position + small offset
64
+ for (const id of sections) {
65
+ const element = document.getElementById(id);
66
+ if (element) {
67
+ const rect = element.getBoundingClientRect();
68
+ const absoluteTop = rect.top + windowTop;
69
+
70
+ if (absoluteTop > windowTop + 100) {
71
+ nextSectionId = id;
72
+ break;
73
+ }
74
+ }
75
+ }
76
+
77
+ if (nextSectionId) {
78
+ const element = document.getElementById(nextSectionId);
79
+ element.scrollIntoView({ behavior: "smooth" });
80
+ } else {
81
+ // If no more sections, just go to bottom
82
+ window.scrollTo({
83
+ top: document.documentElement.scrollHeight,
84
+ behavior: "smooth",
85
+ });
86
+ }
87
+ }
88
+ };
89
+
90
+ return (
91
+ <button
92
+ className={`${styles.navArrow} ${isVisible ? styles.visible : ""} ${isScrolling ? styles.scrolling : ""}`}
93
+ onClick={handleClick}
94
+ aria-label={
95
+ direction === "down" ? "Scroll to next section" : "Scroll to top"
96
+ }
97
+ >
98
+ <Tooltip
99
+ msg={direction === "down" ? "Next Section" : "Back to Top"}
100
+ position="top"
101
+ gap={25}
102
+ underline={false}
103
+ >
104
+ <div className={`${styles.iconWrapper} ${styles[direction]}`}>
105
+ {direction === "down" ? (
106
+ <FaChevronDown className={styles.chevron} />
107
+ ) : (
108
+ <FaChevronUp className={styles.chevron} />
109
+ )}
110
+ </div>
111
+ </Tooltip>
112
+ </button>
113
+ );
114
+ }
@@ -0,0 +1,107 @@
1
+ :root {
2
+ --nav-arrow-size: 50px;
3
+ --nav-arrow-bottom: 2.5rem;
4
+ --nav-arrow-mobile-size: 46px;
5
+ --nav-arrow-mobile-bottom: 1.5rem;
6
+ --nav-arrow-bounce-y: -8px;
7
+ }
8
+
9
+ .navArrow {
10
+ position: fixed;
11
+ bottom: var(--nav-arrow-bottom);
12
+ left: 50%;
13
+ transform: translateX(-50%) translateY(100px);
14
+ width: var(--nav-arrow-size);
15
+ height: var(--nav-arrow-size);
16
+ border-radius: 50%;
17
+ background-color: var(--ifm-color-primary);
18
+ color: var(--ifm-color-black);
19
+ border: none;
20
+ display: flex;
21
+ align-items: center;
22
+ justify-content: center;
23
+ cursor: pointer;
24
+ z-index: var(--ifm-z-index-fixed, 1000);
25
+ box-shadow: var(--ifm-global-shadow-lw);
26
+ transition:
27
+ transform var(--ifm-transition-slow) cubic-bezier(0.175, 0.885, 0.32, 1.275),
28
+ opacity var(--ifm-transition-slow) ease,
29
+ background-color var(--ifm-transition-fast) ease;
30
+ opacity: 0;
31
+ pointer-events: none;
32
+ }
33
+
34
+ .navArrow.visible {
35
+ transform: translateX(-50%) translateY(0);
36
+ opacity: 1;
37
+ pointer-events: auto;
38
+ }
39
+
40
+ .navArrow:hover {
41
+ background-color: var(--ifm-color-primary-darker);
42
+ transform: translateX(-50%) translateY(-5px) scale(1.1);
43
+ box-shadow: var(--ifm-global-shadow-md);
44
+ }
45
+
46
+ .navArrow:active {
47
+ transform: translateX(-50%) translateY(-2px) scale(0.95);
48
+ }
49
+
50
+ .iconWrapper {
51
+ display: flex;
52
+ align-items: center;
53
+ justify-content: center;
54
+ }
55
+
56
+ .down {
57
+ animation: bounce 2s infinite;
58
+ }
59
+
60
+ .chevron {
61
+ font-size: 1.4rem;
62
+ color: inherit;
63
+ }
64
+
65
+ @keyframes bounce {
66
+ 0%,
67
+ 20%,
68
+ 50%,
69
+ 80%,
70
+ 100% {
71
+ transform: translateY(0);
72
+ }
73
+ 40% {
74
+ transform: translateY(var(--nav-arrow-bounce-y));
75
+ }
76
+ 60% {
77
+ transform: translateY(calc(var(--nav-arrow-bounce-y) / 2));
78
+ }
79
+ }
80
+
81
+ /* Subtly fade during manual scroll */
82
+ .navArrow.scrolling {
83
+ opacity: 0.7;
84
+ }
85
+
86
+ @media (max-width: 768px) {
87
+ .navArrow {
88
+ bottom: var(--nav-arrow-mobile-bottom);
89
+ width: var(--nav-arrow-mobile-size);
90
+ height: var(--nav-arrow-mobile-size);
91
+ }
92
+
93
+ .chevron {
94
+ font-size: 1.2rem;
95
+ }
96
+ }
97
+
98
+ /* Reduced motion support */
99
+ @media (prefers-reduced-motion: reduce) {
100
+ .navArrow {
101
+ transition: opacity var(--ifm-transition-fast) ease;
102
+ animation: none !important;
103
+ }
104
+ .navArrow.visible {
105
+ transform: translateX(-50%) translateY(0);
106
+ }
107
+ }