@@ -107,103 +107,227 @@ <h1>Make Smarter <br /><span class="gradient-text">Tech Decisions.</span>
107107 Just clear guidance on the tech that powers modern work.</ p >
108108
109109 </ div >
110- <!-- Hero Featured Products Carousel -->
111- < div class ="hero-visual slider-container ">
110+ <!-- Pull-Out Animated Deck -->
111+ < div class ="hero-visual ">
112112 < style >
113- .product-slider {
114- display : flex;
115- gap : 1.5rem ;
116- overflow-x : auto;
117- scroll-snap-type : x mandatory;
118- padding-bottom : 1rem ;
119- scrollbar-width : none;
120- /* Firefox */
121- -ms-overflow-style : none;
122- /* IE and Edge */
123- max-width : 100% ;
124- }
125-
126- .product-slider ::-webkit-scrollbar {
127- display : none;
113+ .deck-container {
114+ position : relative;
115+ width : 100% ;
116+ max-width : 400px ;
117+ height : 520px ;
118+ margin : 0 auto;
119+ perspective : 1200px ;
120+ transform-style : preserve-3d;
128121 }
129-
130- .product-slider > .glass-card {
131- scroll-snap-align : center;
132- min-width : 280px ;
133- max-width : 300px ;
134- flex : 0 0 auto;
122+ .deck-card {
123+ position : absolute;
124+ top : 0 ; left : 0 ; right : 0 ;
125+ background : rgba (15 , 23 , 42 , 0.7 );
126+ backdrop-filter : blur (16px );
127+ border : 1px solid var (--border-glass );
128+ border-top : 4px solid var (--accent );
129+ border-radius : var (--border-radius );
135130 padding : 2rem ;
136- border-top : 4px solid var (--accent , # 38bdf8 );
137- box-shadow : 0 20px 40px rgba (0 , 0 , 0 , 0.4 );
131+ height : 480px ;
138132 display : flex;
139133 flex-direction : column;
134+ cursor : grab;
135+ user-select : none;
136+ will-change : transform, opacity;
137+ box-shadow : 0 25px 50px -12px rgba (0 , 0 , 0 , 0.5 );
138+ /* Smooth transitions when dropping or clicking next */
139+ transition : transform 0.4s cubic-bezier (0.2 , 0.8 , 0.2 , 1 ), opacity 0.4s linear;
140140 }
141-
142- .product-slider > .glass-card h3 {
143- font-size : 1.3rem ;
144- margin-bottom : 0.5rem ;
145- line-height : 1.3 ;
141+ .deck-card : active {
142+ cursor : grabbing;
146143 }
147-
148- .product-slider > .glass-card p {
149- color : var (--text-secondary , # 94a3b8 );
150- font-size : 0.9rem ;
151- margin-bottom : 1.5rem ;
152- line-height : 1.5 ;
153- flex-grow : 1 ;
144+ .deck-card img {
145+ width : 100% ; height : auto; border-radius : 8px ; margin-bottom : 1rem ; aspect-ratio : 16 / 9 ; object-fit : cover; pointer-events : none;
154146 }
147+ .deck-card h3 { font-size : 1.4rem ; margin-bottom : 0.5rem ; line-height : 1.3 ; color : white; }
148+ .deck-card p { color : var (--text-secondary ); font-size : 0.95rem ; margin-bottom : 1.5rem ; line-height : 1.5 ; pointer-events : none; flex-grow : 1 ; }
149+
150+ /* The pull indicator */
151+ .pull-indicator {
152+ position : absolute; top : -30px ; right : 10px ; font-size : 0.85rem ; color : var (--accent ); opacity : 0 ;
153+ font-weight : 600 ; text-transform : uppercase; letter-spacing : 1px ; z-index : 10 ;
154+ animation : bounceX 2s infinite ease-in-out;
155+ pointer-events : none;
156+ }
157+ @keyframes bounceX {
158+ 0% , 100% { transform : translateX (0 ); opacity : 0.8 ;}
159+ 50% { transform : translateX (-10px ); opacity : 0.4 ;}
160+ }
161+
162+ /* Deck states based on data-depth attributes controlled by JS */
163+ .deck-card [data-depth = "0" ] { transform : translateZ (0 ) translateY (0 ) scale (1 ); z-index : 3 ; opacity : 1 ; }
164+ .deck-card [data-depth = "1" ] { transform : translateZ (-80px ) translateY (25px ) scale (0.95 ); z-index : 2 ; opacity : 0.8 ; }
165+ .deck-card [data-depth = "2" ] { transform : translateZ (-160px ) translateY (50px ) scale (0.9 ); z-index : 1 ; opacity : 0.4 ; }
166+
167+ /* Thrown away states */
168+ .deck-card .throw-left { transform : translateZ (0 ) translateX (-150% ) rotate (-15deg ) !important ; opacity : 0 !important ; }
169+ .deck-card .throw-right { transform : translateZ (0 ) translateX (150% ) rotate (15deg ) !important ; opacity : 0 !important ; }
155170
156- .product-slider > .glass-card img {
157- width : 100% ;
158- height : auto;
159- border-radius : 8px ;
160- margin-bottom : 1rem ;
161- aspect-ratio : 16 / 9 ;
162- object-fit : cover;
171+ /* Controls */
172+ .deck-controls {
173+ display : flex; justify-content : space-between; align-items : center; max-width : 400px ; margin : 0 auto; position : relative; z-index : 10 ;
163174 }
175+ .deck-controls button {
176+ background : rgba (255 , 255 , 255 , 0.05 ); border : 1px solid var (--border-glass ); color : white; width : 45px ; height : 45px ; border-radius : 50% ; cursor : pointer; transition : 0.2s ; display : flex; align-items : center; justify-content : center;
177+ }
178+ .deck-controls button : hover { background : var (--accent ); color : # 000 ; }
179+ .deck-controls button i { font-size : 1.1rem ; }
164180 </ style >
165- < div class ="product-slider ">
166- <!-- Sony WH-1000XM5 -->
167- < div class ="glass-card ">
168- < div
169- style ="font-size: 0.75rem; color: var(--accent, #38bdf8); font-weight: bold; text-transform: uppercase; margin-bottom: 0.5rem; ">
181+
182+ < div class ="deck-container " id ="reviewDeck ">
183+ <!-- The indicator shows up to hint users to swipe -->
184+ < div class ="pull-indicator " id ="pullHint "> ← Pull to next</ div >
185+
186+ <!-- Card 0: Sony -->
187+ < div class ="deck-card " data-index ="0 ">
188+ < div style ="font-size: 0.75rem; color: var(--accent); font-weight: bold; text-transform: uppercase; margin-bottom: 0.5rem; ">
170189 < i class ="fa-solid fa-headphones "> </ i > Audio
171190 </ div >
172191 < img src ="posts/images/sony-wh-1000xm5-front.jpg " alt ="Sony WH-1000XM5 " loading ="lazy ">
173192 < h3 > Sony WH-1000XM5</ h3 >
174- < p > Top-tier active noise cancellation and supreme all-day comfort for deep work.</ p >
175- < a class ="read-more " href ="posts/sony-wh-1000xm5-review.html "
176- style ="font-weight: 700; margin-top: auto; "> Read Review < i class ="fa-solid fa-arrow-right "
177- style ="margin-left: 5px; "> </ i > </ a >
193+ < p > Top-tier active noise cancellation and supreme all-day comfort for deep focus sessions.</ p >
194+ < a class ="read-more " href ="posts/sony-wh-1000xm5-review.html " style ="font-weight: 700; display:inline-block; margin-top: auto; "> Read Review < i class ="fa-solid fa-arrow-right " style ="margin-left: 5px; "> </ i > </ a >
178195 </ div >
179196
180- <!-- Shure SM7dB -->
181- < div class ="glass-card ">
182- < div
183- style ="font-size: 0.75rem; color: var(--accent, #38bdf8); font-weight: bold; text-transform: uppercase; margin-bottom: 0.5rem; ">
197+ <!-- Card 1: Shure -->
198+ < div class ="deck-card " data-index ="1 ">
199+ < div style ="font-size: 0.75rem; color: var(--accent); font-weight: bold; text-transform: uppercase; margin-bottom: 0.5rem; ">
184200 < i class ="fa-solid fa-microphone "> </ i > Broadcasting
185201 </ div >
186202 < img src ="posts/images/shure-sm7db-primary.jpg " alt ="Shure SM7dB Microphone " loading ="lazy ">
187203 < h3 > Shure SM7dB</ h3 >
188- < p > The industry standard podcast microphone, now with a built-in clean preamp.</ p >
189- < a class ="read-more " href ="posts/shure-sm7db-review.html " style ="font-weight: 700; margin-top: auto; "> Read
190- Review < i class ="fa-solid fa-arrow-right " style ="margin-left: 5px; "> </ i > </ a >
204+ < p > The industry standard podcast microphone, now powered with a built-in clean preamp.</ p >
205+ < a class ="read-more " href ="posts/shure-sm7db-review.html " style ="font-weight: 700; display:inline-block; margin-top: auto; "> Read Review < i class ="fa-solid fa-arrow-right " style ="margin-left: 5px; "> </ i > </ a >
191206 </ div >
192207
193- <!-- Alienware AW3423DWF -->
194- < div class ="glass-card ">
195- < div
196- style ="font-size: 0.75rem; color: var(--accent, #38bdf8); font-weight: bold; text-transform: uppercase; margin-bottom: 0.5rem; ">
208+ <!-- Card 2: Alienware -->
209+ < div class ="deck-card " data-index ="2 ">
210+ < div style ="font-size: 0.75rem; color: var(--accent); font-weight: bold; text-transform: uppercase; margin-bottom: 0.5rem; ">
197211 < i class ="fa-solid fa-desktop "> </ i > Displays
198212 </ div >
199213 < img src ="posts/images/alienware-aw3423dwf-front.jpg " alt ="Alienware AW3423DWF " loading ="lazy ">
200214 < h3 > Alienware AW3423DWF</ h3 >
201- < p > Stunning QD-OLED ultrawide performance for flawless visual immersion.</ p >
202- < a class ="read-more " href ="posts/alienware-aw3423dwf-review.html "
203- style ="font-weight: 700; margin-top: auto; "> Read Review < i class ="fa-solid fa-arrow-right "
204- style ="margin-left: 5px; "> </ i > </ a >
215+ < p > Stunning QD-OLED ultrawide performance delivering flawless visual immersion.</ p >
216+ < a class ="read-more " href ="posts/alienware-aw3423dwf-review.html " style ="font-weight: 700; display:inline-block; margin-top: auto; "> Read Review < i class ="fa-solid fa-arrow-right " style ="margin-left: 5px; "> </ i > </ a >
205217 </ div >
206218 </ div >
219+
220+ < div class ="deck-controls ">
221+ < button id ="deckPrev " aria-label ="Previous Review "> < i class ="fa-solid fa-arrow-left "> </ i > </ button >
222+ < span style ="font-size: 0.9rem; color: var(--text-muted); font-weight: 600; letter-spacing: 1px; " id ="deckCount "> 1 / 3</ span >
223+ < button id ="deckNext " aria-label ="Next Review "> < i class ="fa-solid fa-arrow-right "> </ i > </ button >
224+ </ div >
225+
226+ < script >
227+ document . addEventListener ( 'DOMContentLoaded' , ( ) => {
228+ const deck = document . getElementById ( 'reviewDeck' ) ;
229+ if ( ! deck ) return ;
230+
231+ const cards = Array . from ( deck . querySelectorAll ( '.deck-card' ) ) ;
232+ const prevBtn = document . getElementById ( 'deckPrev' ) ;
233+ const nextBtn = document . getElementById ( 'deckNext' ) ;
234+ const countEl = document . getElementById ( 'deckCount' ) ;
235+ const hint = document . getElementById ( 'pullHint' ) ;
236+
237+ let topIndex = 0 ;
238+ const total = cards . length ;
239+
240+ function updateDeck ( ) {
241+ cards . forEach ( ( card , i ) => {
242+ card . classList . remove ( 'throw-left' , 'throw-right' ) ;
243+ card . style . transition = '' ; // reset to CSS transition
244+ card . style . transform = '' ; // clears inline drag transforms
245+
246+ // Distance from current top card
247+ let depth = ( i - topIndex + total ) % total ;
248+ card . setAttribute ( 'data-depth' , depth . toString ( ) ) ;
249+ } ) ;
250+ countEl . textContent = `${ topIndex + 1 } / ${ total } ` ;
251+ }
252+
253+ function throwCard ( direction ) {
254+ const currentTopCard = cards [ topIndex ] ;
255+ // Apply the throw animation class
256+ currentTopCard . classList . add ( direction === 'left' ? 'throw-left' : 'throw-right' ) ;
257+
258+ // Hide hint after first interaction
259+ if ( hint ) hint . style . display = 'none' ;
260+
261+ // Wait enough time for the throw animation to clear visually before re-stacking
262+ setTimeout ( ( ) => {
263+ if ( direction === 'left' || direction === 'next' ) {
264+ topIndex = ( topIndex + 1 ) % total ;
265+ } else {
266+ topIndex = ( topIndex - 1 + total ) % total ;
267+ }
268+ updateDeck ( ) ;
269+ } , 300 ) ; // 300ms matches CSS transition timing
270+ }
271+
272+ nextBtn . addEventListener ( 'click' , ( ) => throwCard ( 'left' ) ) ;
273+ prevBtn . addEventListener ( 'click' , ( ) => throwCard ( 'right' ) ) ;
274+
275+ // Mouse / Touch Drag Support
276+ let isDragging = false ;
277+ let startX = 0 ;
278+ let currentX = 0 ;
279+
280+ function onStart ( x , y , target ) {
281+ // Ignore clicks on links/buttons
282+ if ( target . closest ( 'a' ) || target . closest ( 'button' ) ) return ;
283+ isDragging = true ;
284+ startX = x ;
285+ currentX = x ;
286+ // Remove transition visually to make dragging instantaneous
287+ cards [ topIndex ] . style . transition = 'none' ;
288+
289+ if ( hint ) hint . style . display = 'none' ;
290+ }
291+
292+ function onMove ( x ) {
293+ if ( ! isDragging ) return ;
294+ currentX = x ;
295+ const diffX = currentX - startX ;
296+ const rotate = diffX * 0.05 ;
297+ // Instantly update transform
298+ cards [ topIndex ] . style . transform = `translateZ(0) translateY(0) translateX(${ diffX } px) rotate(${ rotate } deg)` ;
299+ }
300+
301+ function onEnd ( ) {
302+ if ( ! isDragging ) return ;
303+ isDragging = false ;
304+ const diffX = currentX - startX ;
305+
306+ if ( Math . abs ( diffX ) > 80 ) { // threshold passed
307+ throwCard ( diffX < 0 ? 'left' : 'right' ) ;
308+ } else {
309+ // return to center smoothly
310+ updateDeck ( ) ;
311+ }
312+ }
313+
314+ deck . addEventListener ( 'mousedown' , ( e ) => onStart ( e . clientX , e . clientY , e . target ) ) ;
315+ window . addEventListener ( 'mousemove' , ( e ) => onMove ( e . clientX ) ) ;
316+ window . addEventListener ( 'mouseup' , onEnd ) ;
317+
318+ deck . addEventListener ( 'touchstart' , ( e ) => onStart ( e . touches [ 0 ] . clientX , e . touches [ 0 ] . clientY , e . target ) , { passive : false } ) ;
319+ deck . addEventListener ( 'touchmove' , ( e ) => {
320+ if ( isDragging ) { e . preventDefault ( ) ; onMove ( e . touches [ 0 ] . clientX ) ; }
321+ } , { passive : false } ) ;
322+ deck . addEventListener ( 'touchend' , onEnd ) ;
323+
324+ // Init
325+ updateDeck ( ) ;
326+
327+ // Show hint briefly on load
328+ setTimeout ( ( ) => { if ( hint ) hint . style . opacity = '1' ; } , 1500 ) ;
329+ } ) ;
330+ </ script >
207331 </ div >
208332 </ section >
209333 <!-- Featured Categories-->
0 commit comments