Profile
Back to NewsBack
GitHub Trending 18 min
Reader Mode
Bium23/NxLibrary: A console-style macOS game library and launcher in SwiftUI. Features guided setup, assisted local builds for nxnative, save backup/import, and controller navigation on Apple Silicon.

Bium23/NxLibrary: A console-style macOS game library and launcher in SwiftUI. Features guided setup, assisted local builds for nxnative, save backup/import, and controller navigation on Apple Silicon.

NX Library app icon NX Library

A home for your games on Mac.

Organise your library, build your own Switch dumps with nxnative,
and launch the resulting Mac apps from one place.

Status: alpha macOS 13 or later Apple silicon MIT license

Download · Get started · User guide · Troubleshooting · Changelog · Next steps · Development

!NX Library Spotlight with a selected game, artwork, play history and library navigation

Your library, together.
Games, artwork and activity in Spotlight.

Overview

NX Library is an independent SwiftUI frontend for the external nxnative toolchain. It brings your installed game apps, local build workflow and game options into a modern Mac-native library inspired by Selene.

| Area | What you can do | |---|---| | Library | Browse Spotlight and Collection, search, favourite, hide and reorder games. | | Local builds | Select your own sources, review updates and DLC, follow progress and retry a failed installation. | | Game options | Keep each title's launch settings, artwork and metadata together. | | Saves | Back up local progress, preview imports and restore a previous copy. | | Cheats | Import matching cheat files and choose what is enabled for the next launch. | | Personalisation | Choose palettes, wallpapers, live backgrounds, interface sounds and controller prompts. |

Game files, keys and firmware are not provided. See Legal and third-party notices.

Project status

0.1.3.3 alpha. Download the packaged app from Releases, or build it from source.

Automated checks cover the application code, publication inputs, compilation and development-app packaging. They do not certify game compatibility. Changes are recorded in the changelog, and the current checks are available under Actions.

The engine is pinned to b64a5ff0, with audio, memory and renderer fixes, updated game profiles and shader-build improvements, using Xcode 27.2 / Metal 32023.922. See the engine upgrade notes.

Getting started

Requirements

| Requirement | Details | |---|---| | Mac | Apple silicon, macOS 13 or later. | | Xcode | 27.2 or later, with the Metal compiler certified for the pinned engine. | | .NET | .NET 10 SDK, used by the engine's extractor. | | Engine | nxnative at the pinned commit. Setup can prepare the checkout; an existing folder can be chosen in Settings → Engine. | | Your files | Your own supported game sources and keys, with the rights and permissions needed to use them. |

A packaged app includes Python and its required packages, zstd and glslangValidator. Homebrew is used to prepare that runtime during development; it is not needed to use a packaged copy. A source build without the bundled runtime uses the engine's virtual environment and tools on PATH.

Building from source

With the development prerequisites installed:

~~~bash git clone https://github.com/Bium23/NxLibrary.git cd NxLibrary

bash scripts/bundle_runtime.sh bash scripts/build_app.sh open "$HOME/Applications/NXLibrary.app" ~~~

Run bundle_runtime.sh once, and again when runtime.lock changes. build_app.sh prepares, signs and verifies the app before installing it atomically in ~/Applications.

Build options, runtime dependencies, debug builds and release packaging are documented in the development guide.

Installing a packaged release

  1. Download the DMG from Releases.
  2. Open it and drag NX Library to Applications.
  3. Open the app and complete Setup.
The current alpha is ad-hoc signed and not notarised. If macOS blocks first opening, use System Settings → Privacy & Security → Open Anyway only for a copy you built or obtained from a source you trust.

Packaged releases check this repository for app updates; no GitHub account is needed. Choose Update to download, verify, install and reopen NX Library while keeping your library and preferences. Automatic checks run at most once every 24 hours; NX Library → Check for Updates… in the macOS menu bar and Settings → About → Check now check immediately. Both open the same app-style dialog, with scrollable release notes and a visible result even when no update is available. Updates wait while a build, a running game or a save operation requires the app. Development builds can check public releases manually but never replace themselves.

First-run setup

Setup has two steps, Get ready and Your games.

Get ready shows the engine, build tools and keys. Install all handles the supported steps: the engine checkout, the certified Metal compiler and the official .NET installer. Xcode itself comes from the App Store; its licence and any system prompts remain your responsibility. Choose Continue to proceed; incomplete build requirements can be finished later, and existing game apps remain playable.

Your games shows Game apps: where to keep existing nxnative games (.app) and install new builds. It checks the selected folder before reporting an empty library. Use Change to choose another folder, Choose game file for NSP, XCI, NSZ or XCZ, or Choose a folder instead for a source folder, including supported extracted inputs. A single detected title opens the existing build review; multiple titles require a choice. Selecting sources never starts a build by itself. Cancelling the picker stays on this page.

If games are found, Open library takes you to them. Skip for now enters the launcher without adding a game and is remembered: the welcome flow does not return automatically just because the library is empty. Add games later from Spotlight's Add Game card or Collection. An interrupted first setup resumes on its current step.

The default Game apps folder is ~/Applications; first use can prepare this default without an extra picker. Remembered or custom unavailable folders are never recreated automatically. Changing folders does not move installed apps, and a rebuild updates an existing app in its current location. A read-only folder still allows discovery and Play; installing new builds needs write access.

Discovery includes subfolders of Game apps. It stays inside the chosen folders, skips app contents and limits how far it searches. Search limits and unreadable folders are explained beside Settings → Library → Scan game apps, without interrupting launch. Existing library entries are kept. If several builds of a title are found, the registered app is retained while it exists; a newly discovered title uses the most recently modified build.

Game files is a separate optional folder preference in Settings → Library. Selecting individual files for a porting does not replace that preference or scan their siblings. Both folder choices can be changed in Settings later.

Import your prod.keys when prompted. NX Library keeps a private copy with mode 0600 and leaves the original file unchanged. Key values are not displayed or logged.

Reopen both setup steps from Settings → Start-up → Open Setup whenever needed; this keeps your library and completion state. NX Library never runs sudo, accepts the Xcode licence for you or changes xcode-select.

Already have a built nxnative game app? Use Settings → Library → Scan game apps → Scan. The same action finds existing builds and refreshes the library. Play remains available without the original sources; rebuilding requires attaching them again.

If an installed app is moved or removed, its library entry stays with a blurred, locked tile and App not found. Restore it to its folder or change Game apps, then refresh the library. NX Library does not search elsewhere for it. Missing build inputs show Game files not found when there is no app to play. An existing app still plays when its source files are missing; only rebuilding is unavailable. To reconnect a game's moved inputs, use Game Options → Edit details → Attach sources….

Using NX Library

Your library

Spotlight places your games in a row of square tiles, with Play, Options and Favourite beside the selected title. Scroll down for descriptions, screenshots, activity and build information. Collection offers a cover-based view.

!Game details with screenshots, a description, play activity and build information

A closer look at each game.
Artwork, play history and build details in one place.

Use Find a game for search, Game Options for title-specific actions, and the menu for themes, Settings and running-game controls. Hidden games can be restored under Settings → Games → Hidden games.

!The quick menu open over Spotlight, with Theme selected

Quick access.
Open the menu without leaving your library.

Add or rebuild a game

  1. Open Add game and choose or drop a supported file or folder.
  2. Review the detected base game, update and DLC. Supported source formats are NSP, XCI, NSZ and XCZ.
  3. Check the destination, disk estimate and game options.
  4. Choose Add and port to build, or Add to library only to keep the entry for later.
For a title already in the library, Review opens with that game's saved options and offers Rebuild or Save changes.

Sources stay in place; staged workspaces use symbolic links. New game apps install in the chosen Game apps folder. Builds run on the drive the app goes to: with the folder on an external APFS or Mac OS Extended drive, the extracted game, shader cache and new app stay on that drive in a hidden .nxlibrary-build folder, so the Mac's disk needs no space for the build and the app is moved into place, not copied. On an exFAT drive or a network share builds keep using the Mac's disk: exFAT keeps Mac file attributes in separate ._ files, which break a built app's signature. Review shows the actual destination, including any filename needed to avoid an unrelated app. Build stages, progress, estimated time and a live log remain available while the operation runs. The first build of a game after an engine update can first move its shader cache into the engine's new format: that shows Updating shader cache instead of an ETA and takes longer on a large cache or an external drive. Closing the porting screen leaves the operation running; cancellation is a separate action.

A rebuild of a running game waits to replace its app until it closes. If compilation finishes but installation fails, Retry installs the preserved result without compiling again. If the destination folder disconnects, reconnect it before retrying: the ready build keeps its original destination. A library refresh preserves pending installations and failure information.

!A game build in progress in Spotlight, showing its progress and estimated time remaining

Follow the build.
Keep track of porting progress from Spotlight.

Know which settings apply

| Where | Scope | When it applies | |---|---|---| | Settings → Games → New games | Starting values for titles added later. | When a new title is added. Existing titles keep their choices. | | Game Options → Next launch | The selected game. | Its next launch. | | Game Options → Use new game defaults | Copies the current defaults into that game once. | Its next launch. | | Add game → Review | The title being added or rebuilt. | That operation, including its saved language. | | Settings → Games → Cloud saves | Library-wide preference, subject to advanced game configuration overrides. | Reapplied before each launch, including when Off. | | Settings → Look | The library's presentation. | The app interface; game artwork and launch-screen style have their own controls. |

Options include fullscreen, FPS target, docked mode, language, rumble, render scale, MetalFX upscaling and Metal HUD. MetalFX upscaling enlarges the finished picture, HUD included, to your window or screen. Game's upscaler, offered only by games that upscale their own picture inside the frame (such as Donkey Kong Bananza), keeps the game's own upscaler or swaps it for MetalFX's, which takes much less GPU time; both can be on. Some controls depend on the installed build's capabilities. A control being available does not guarantee that every game supports its effect.

Settings includes Search and Changed. Group resets retain the cloud-sync choice and selected sources folder. Descriptions explain the scope; information buttons provide additional detail.

!Settings with categories, Start-up preferences and access to Setup

Settings by area.
Find library, appearance, controller and start-up preferences.

Saves: back up, import, restore

Open Game Options → Saves, directly below Cheats. Keep the game closed during backup, import or restore.

| Button | What it does | |---|---| | Open folder | Opens the current local save in Finder. | | Back up now | Creates a dated, verified copy of account and device saves. | | Save type | Selects the import destination. User save is the usual choice; Device save is for a separate device save. | | Import save… | Previews an exported save folder or a dated NX Library backup before replacing anything. | | Refresh | Reads save and backup information again. If recovery is needed, offers Retry recovery. | | Backup folder | Opens this game's backups in Finder. The row shows the latest backup's date and time. |

To import: choose the folder, review the centred From → To preview, then confirm. NX Library backs up existing progress first and verifies the result. A successful import shows Imported successfully, which disappears after three seconds.

For Ryujinx, choose the numbered game folder under bis/user/save/. A folder selected inside that tree is recognised too; NX Library checks its title metadata and imports the complete committed save, including all slots. For other exports, choose the folder containing the complete save.

To restore: choose a dated backup folder through Import save…. A restore reinstates both recorded destinations, including whether either one was absent. An ordinary import replaces the selected destination.

Cloud sync and backups have different purposes. Cloud sync can replace local files, so import and restore require it to be off in the effective configuration. A backup is a dated copy you manage in Finder. Save compatibility still depends on the game, platform and version; recognising the folder does not guarantee compatibility.

Interrupted replacements must recover before Play. The Saves panel reports what is blocking an operation and keeps the backup folder accessible.

Cheats

Open Game Options → Cheats, then choose or drop a matching cheat file. NX Library checks the title/build information; newly imported cheats start off. Enable the master switch and the individual cheats you want, then launch the game.

Changes apply at the next Play and are written only while the game is closed. An older or unrecognised engine build may require Rebuild. The engine's in-game panel, ⌘⇧C, reads the same files.

Artwork, themes and sound

  • Game Options → Edit details changes one game's artwork or metadata. Manual artwork choices take priority over automatic images.
  • Menu → Theme chooses palettes and wallpapers. Background choices include local images, muted looping videos and optional online sources; source credits appear in the picker.
  • Settings → Look controls wallpaper placement, sizing, brightness, game backgrounds, Reduce Motion and the Color/Monochrome app icon.
  • Settings → Sound & music controls interface cues and volume. Four interface sounds are bundled; additional sounds and wallpapers can be imported from a Selene theme folder you own.
Online artwork and wallpapers require their providers to be available. Their content and service terms remain those of the respective owners.

!The colour theme picker with palettes and a live library preview

Choose your colours.
Preview each palette before applying it.

!The static wallpaper browser with categories, image thumbnails and a library preview

Find a wallpaper.
Browse backgrounds and preview them in your library.

!The live background picker with online sources, local videos and a library preview

Add a little motion.
Choose a live background or bring your own video.

Controls

Settings → Start-up → Start in fullscreen chooses the launcher window mode at the next opening. It is separate from each game’s Fullscreen option.

The macOS Game menu follows the current selection. Game Details opens the existing game page in Spotlight or the selected cover’s details in Collection; Game Options… opens the same options as the in-app button. Unavailable commands are disabled.

In fullscreen, the pointer hides after three seconds without movement. Moving the mouse reveals it; keyboard/controller navigation, clicks and scrolling do not. Leaving fullscreen, switching apps or opening a native system panel restores it.

| Action | Keyboard | Controller | |---|---|---| | Navigate | Arrow keys | D-pad / left stick | | Select | Return / Space | Confirm button | | Back | Escape | Back button | | Menu | ⌘K | Menu / Start | | Settings | ⌘, | Hold Menu | | Find a game | ⌘F | Hold View | | Switch library view | Tab | View | | Switch tabs / filters | Q / E where shown | LB / RB |

Prompts follow the connected controller and supported system remapping. Settings → Controllers → Button layout separates Library navigation from In game defaults. Game inputs are handled by the engine and can differ in older builds.

Troubleshooting

| What you see | What to do | |---|---| | Play works, Rebuild is blocked | Open Settings → Engine and inspect the gate. Existing apps and new builds have separate requirements. | | Sources detached / missing | Use Attach Sources… or locate the original files. The installed app remains usable. | | Ready build waiting to install | Close the running game or finish the save operation, then retry the installation if prompted. | | Import is unavailable | Close the game, finish any active operation and follow the cloud-sync or recovery action shown in Saves. | | Import fails | Read the error message, check that the folder contains a complete save and inspect the preview before retrying. | | A setting cannot be saved | Use the dialog's Retry. Unreadable preferences are preserved before defaults are written. | | Porting fails | Use the explained failure and its suggested action. Console contains the build log and Copy diagnostics. |

Before reporting a problem, review copied diagnostics and remove anything personal. Include your app/macOS versions, the operation and steps to reproduce. Read SECURITY.md before sharing a suspected vulnerability.

Compatibility and limitations

  • NX Library uses a manually pinned engine and checks its commit, working tree, CLI contract and Metal compiler before new builds. It does not follow upstream main automatically. Settings → Engine → Newest engine lets you choose main yourself before NX Library has tested it: an orange dot marks it, builds are recorded as untested, Back to tested returns to the pinned engine, and it is not offered when main needs a different Metal compiler.
  • The current engine is bd979ced and certifies Metal 32023.922. Build anyway with an untested toolchain requires explicit confirmation and marks the resulting build as untested. A successful launch does not turn it into a certified combination.
  • Game compatibility depends on nxnative, the game version and the toolchain.
  • Output follows the window/display; native 1440p/4K internal rendering is not supported. Render scale is 50–100% where the build supports it. MetalFX is spatial upscaling.
  • Builds run one at a time. iOS/IPA output, automatic engine updates and notarisation are not currently provided.
The upgrade note records the engine review and toolchain requirements.

Data and cleanup

App data lives in ~/Library/Application Support/NXLibrary/. Local game data uses ~/Library/Application Support/nxnative//</code>. Backups are stored under <code>NXLibrary/SaveBackups/<title ID>/</code>.</p> <p>Storage can clear caches and logs and open the shared backups folder. <strong>Remove Sources & Build Cache deletes the selected original source files</strong>, while retaining the installed app and saves. Read that confirmation carefully; it differs from clearing a cache.</p> <h2>Next steps</h2> <ul><li><strong>Simpler Settings</strong> — Keep everyday options easy to find, with advanced controls grouped within each category.</li> <li><strong>In-app bug reporting</strong> — Make it easier to report a problem directly from NX Library.</li> <li><strong>Broader game discovery</strong> — Discover and launch games from Steam, CrossOver and the Mac App Store, alongside nxnative apps.</li> <li><strong>An in-app mod browser</strong> — Browse, download and install game mods without leaving the app.</li> <li><strong>Better controller support</strong> — Refine controller settings and offer more flexible button mapping.</li> <li><strong>Continued polish</strong> — Fix bugs and improve navigation, feedback and visual details throughout the app.</li></ul> More to come as NX Library evolves. <h2>Documentation and development</h2> <p>| Document | Contents | |---|---| | <a href="CHANGELOG.md" target="_blank" rel="noopener">Changelog</a> | User-visible changes and release history. | | <a href="docs/development.md" target="_blank" rel="noopener">Development guide</a> | Runtime preparation, debug builds, tests, packaging and releases. | | <a href="docs/media/README.md" target="_blank" rel="noopener">Screenshots and demo</a> | Screenshot inventory, placement and the upcoming video. | | <a href="CONTRIBUTING.md" target="_blank" rel="noopener">Contributing</a> | Change and verification guidelines. | | <a href="SECURITY.md" target="_blank" rel="noopener">Security</a> | Private vulnerability reporting and handling sensitive files. | | <a href="docs/architecture.md" target="_blank" rel="noopener">Technical guide</a> | Current architecture and feature flows, in Italian. | | <a href="docs/file-map.md" target="_blank" rel="noopener">File catalog</a> | Responsibility of every project file, in Italian. | | <a href="docs/upstream-upgrades/README.md" target="_blank" rel="noopener">Engine upgrade notes</a> | Pin changes and their evidence. |</p> <h2>Credits and licences</h2> <p>NX Library is MIT licensed. It uses upstream <a href="https://github.com/seedds/nxnative" target="_blank" rel="noopener">nxnative</a>. It began from the MIT nxnative_Launcher codebase, which has been rewritten since; that repository is no longer available and its notice stays in <a href="NOTICE" target="_blank" rel="noopener">NOTICE</a>. Adaptations from <a href="https://github.com/LNPANDY/NX2Apple" target="_blank" rel="noopener">NX2Apple</a> cover key checks, failure explanations, diagnostics and updates.</p> <p>The interface draws from the MIT <a href="https://github.com/mattxslv/Selene" target="_blank" rel="noopener">Selene</a> theme. Bundled fonts, sounds, palettes and runtime components retain their own licences; see <a href="NOTICE" target="_blank" rel="noopener">NOTICE</a>, <a href="Resources/Selene/NOTICE.txt" target="_blank" rel="noopener">media notices</a> and <a href="Resources/Selene/NOTICE-palettes.txt" target="_blank" rel="noopener">palette notices</a>.</p> <p>The app icon is original artwork created by the owner with AI. Its <a href="Resources/IconArtwork.md" target="_blank" rel="noopener">master and generation notes</a> are included.</p> <h2>Legal and third-party notices</h2> <p><strong>Project scope.</strong> NX Library is an independent macOS frontend for organising and launching game applications. Its current nxnative integration also provides guided setup and local builds using the external nxnative toolchain.</p> <p><strong>Games and keys.</strong> The project does not provide games, firmware, cryptographic keys or downloadable game packages. Users must supply their own game files and keys and have the rights and permissions required to use them. Do not submit these files through issues, pull requests or support requests.</p> <p><strong>Local processing and external services.</strong> Game preparation uses locally supplied files and keys. NX Library checks encrypted game headers using the supplied keys and invokes nxnative to perform the build. Setup, updates and optional artwork features may contact third-party services.</p> <p><strong>Permitted use.</strong> Owning a game or console does not necessarily authorise every form of copying, decryption or conversion. Applicable rules vary by jurisdiction. This project does not grant permission to circumvent technological protections or infringe third-party rights. Do not use it to distribute game files, keys or generated packages without the necessary authorisation.</p> <p><strong>Third-party software and content.</strong> NX Library does not distribute proprietary Nintendo game code, game assets, firmware or cryptographic keys. Open-source dependencies and bundled resources remain subject to their respective licences and notices. Artwork retrieved from external services remains subject to its owners’ rights and the applicable service terms.</p> <p><strong>Trademarks.</strong> NX Library is not affiliated with, sponsored by or endorsed by Nintendo or any other game publisher or platform provider. Product names, game titles and trademarks belong to their respective owners.</p> <p><strong>Rights concerns.</strong> If you believe material distributed by this project infringes your rights, contact the maintainer with the relevant material, your rights claim and sufficient information to assess it. Substantiated concerns will be reviewed promptly and addressed as appropriate.</p> <p><strong>No warranty.</strong> NX Library is provided “as is”, without warranty, as described in LICENSE. Third-party components are governed by their own licences. See NOTICE for acknowledgements.</p> </article> <!-- Article Completion Section --> <div class="reader-footer mt-5 pt-5"> <div class="reader-completion-box rounded-3 p-4 text-center"> <div class="reader-completion-icon mb-3"> <i class="fas fa-check-circle"></i> </div> <p class="reader-completion-text fw-semibold mb-1" style="font-size: 1rem;">You finished the article!</p> <p class="reader-completion-subtext small mb-4">Want to go deeper or explore more stories?</p> <div class="d-flex justify-content-center gap-3 flex-wrap"> <a href="https://github.com/Bium23/NxLibrary" target="_blank" rel="noopener noreferrer" class="btn btn-primary-custom btn-sm rounded-pill px-4"> <i class="fas fa-external-link-alt me-2"></i>Full Article on GitHub Trending </a> <a href="https://neshdevtech.com/news" class="reader-back-btn btn btn-sm rounded-pill px-4"> <i class="fas fa-newspaper me-2"></i>More Tech News </a> </div> </div> </div> <!-- Internal Linking CTA --> </div> </div> </div> <!-- Sidebar (desktop only) --> <div class="col-lg-4 col-xl-3 d-none d-lg-block"> <div class="sticky-top" style="top: 80px;"> <!-- Mini Controller Widget --> <div class="card border-0 shadow-sm mb-4 bg-surface"> <div class="card-body p-4"> <h6 class="fw-bold mb-3"><i class="fas fa-glasses me-2 text-primary-custom"></i>Reading Mode</h6> <p class="text-secondary-custom small mb-4">Adjust typography and theme to your preference.</p> <div class="mb-3"> <label class="text-muted-custom small d-block mb-2">Typography</label> <div class="d-flex gap-2"> <button class="btn btn-light btn-sm flex-grow-1 border-custom" id="sidebar-font-serif">Serif</button> <button class="btn btn-light btn-sm flex-grow-1 border-custom" id="sidebar-font-sans">Sans</button> </div> </div> <div class="mb-4"> <label class="text-muted-custom small d-block mb-2">Text Scale</label> <div class="d-flex gap-2"> <button class="btn btn-light btn-sm flex-grow-1 border-custom" id="sidebar-size-dec"><i class="fas fa-minus small"></i></button> <button class="btn btn-light btn-sm flex-grow-1 border-custom" id="sidebar-size-inc"><i class="fas fa-plus small"></i></button> </div> </div> <div class="border-top border-light-custom pt-3"> <a href="https://github.com/Bium23/NxLibrary" target="_blank" rel="noopener noreferrer" class="btn btn-outline-primary btn-sm w-100 rounded-pill"> View Original <i class="fas fa-external-link-alt ms-1"></i> </a> </div> </div> </div> <!-- Sidebar Related News --> <div class="card border-0 shadow-sm bg-surface"> <div class="card-header bg-transparent border-0 pt-4 pb-0 px-4"> <h5 class="card-title fw-bold mb-0" style="font-size: 1.05rem;"> <i class="fas fa-newspaper me-2 text-primary-custom"></i>Related Stories </h5> </div> <div class="card-body p-0"> <div class="list-group list-group-flush rounded-0 bg-transparent"> <a href="https://neshdevtech.com/news/llama-modchip-284b-deepseek-on-64gb-ram-no-gpu-F7pKt" class="list-group-item list-group-item-action border-0 px-4 py-3 bg-transparent border-bottom border-light-custom hover-bg-light"> <h6 class="mb-1 fw-semibold text-truncate-2" style="font-size: 0.875rem; line-height: 1.4;"> Llama-modchip: 284B DeepSeek on 64GB RAM, no GPU </h6> <div class="d-flex align-items-center justify-content-between text-muted-custom mt-1" style="font-size: 0.72rem;"> <span>Hacker News</span> <span>14 hours ago</span> </div> </a> <a href="https://neshdevtech.com/news/chatgpt-silently-passes-data-between-connectors-and-the-bounty-wants-an-id-card-8sU0z" class="list-group-item list-group-item-action border-0 px-4 py-3 bg-transparent border-bottom border-light-custom hover-bg-light"> <h6 class="mb-1 fw-semibold text-truncate-2" style="font-size: 0.875rem; line-height: 1.4;"> ChatGPT silently passes data between connectors and the bounty wants an ID card </h6> <div class="d-flex align-items-center justify-content-between text-muted-custom mt-1" style="font-size: 0.72rem;"> <span>Hacker News</span> <span>2 days ago</span> </div> </a> <a href="https://neshdevtech.com/news/flutter-better-in-java-JoREP" class="list-group-item list-group-item-action border-0 px-4 py-3 bg-transparent border-bottom border-light-custom hover-bg-light"> <h6 class="mb-1 fw-semibold text-truncate-2" style="font-size: 0.875rem; line-height: 1.4;"> Flutter Better in Java </h6> <div class="d-flex align-items-center justify-content-between text-muted-custom mt-1" style="font-size: 0.72rem;"> <span>Dev.to</span> <span>2 days ago</span> </div> </a> <a href="https://neshdevtech.com/news/earliest-evidence-of-cannabis-use-found-in-ancient-siberian-tomb-ctH4d" class="list-group-item list-group-item-action border-0 px-4 py-3 bg-transparent border-bottom border-light-custom hover-bg-light"> <h6 class="mb-1 fw-semibold text-truncate-2" style="font-size: 0.875rem; line-height: 1.4;"> Earliest evidence of cannabis use found in ancient Siberian tomb </h6> <div class="d-flex align-items-center justify-content-between text-muted-custom mt-1" style="font-size: 0.72rem;"> <span>Hacker News</span> <span>2 days ago</span> </div> </a> <a href="https://neshdevtech.com/news/webrtc-calls-key-agreement-and-orm-checks-at-build-time-ww6YL" class="list-group-item list-group-item-action border-0 px-4 py-3 bg-transparent border-bottom border-light-custom hover-bg-light"> <h6 class="mb-1 fw-semibold text-truncate-2" style="font-size: 0.875rem; line-height: 1.4;"> WebRTC Calls, Key Agreement and ORM Checks at Build Time </h6> <div class="d-flex align-items-center justify-content-between text-muted-custom mt-1" style="font-size: 0.72rem;"> <span>Dev.to</span> <span>3 days ago</span> </div> </a> </div> </div> </div> </div> </div> </div> <!-- Mobile: Related News horizontal scroll strip --> <div class="d-block d-lg-none mt-4"> <h6 class="fw-bold mb-3 px-1"> <i class="fas fa-newspaper me-2 text-primary-custom"></i>Related Stories </h6> <div class="related-scroll-strip"> <a href="https://neshdevtech.com/news/llama-modchip-284b-deepseek-on-64gb-ram-no-gpu-F7pKt" class="related-scroll-card"> <p class="related-scroll-title">Llama-modchip: 284B DeepSeek on 64GB RAM, no GPU</p> <div class="related-scroll-meta"> <span>Hacker News</span> <span>14 hours ago</span> </div> </a> <a href="https://neshdevtech.com/news/chatgpt-silently-passes-data-between-connectors-and-the-bounty-wants-an-id-card-8sU0z" class="related-scroll-card"> <p class="related-scroll-title">ChatGPT silently passes data between connectors and the bounty wants a...</p> <div class="related-scroll-meta"> <span>Hacker News</span> <span>2 days ago</span> </div> </a> <a href="https://neshdevtech.com/news/flutter-better-in-java-JoREP" class="related-scroll-card"> <p class="related-scroll-title">Flutter Better in Java</p> <div class="related-scroll-meta"> <span>Dev.to</span> <span>2 days ago</span> </div> </a> <a href="https://neshdevtech.com/news/earliest-evidence-of-cannabis-use-found-in-ancient-siberian-tomb-ctH4d" class="related-scroll-card"> <p class="related-scroll-title">Earliest evidence of cannabis use found in ancient Siberian tomb</p> <div class="related-scroll-meta"> <span>Hacker News</span> <span>2 days ago</span> </div> </a> <a href="https://neshdevtech.com/news/webrtc-calls-key-agreement-and-orm-checks-at-build-time-ww6YL" class="related-scroll-card"> <p class="related-scroll-title">WebRTC Calls, Key Agreement and ORM Checks at Build Time</p> <div class="related-scroll-meta"> <span>Dev.to</span> <span>3 days ago</span> </div> </a> </div> </div> </div> </section> </main> <!-- Footer --> <!-- Footer --> <footer class="py-5 mt-5 position-relative overflow-hidden" style="background: var(--surface); border-top: 1px solid var(--border); box-shadow: var(--shadow-lg);"> <!-- Decorative subtle background gradients --> <div class="position-absolute" style="top: -200px; right: -200px; width: 400px; height: 400px; background: rgba(var(--primary-rgb), 0.03); filter: blur(100px); border-radius: 50%; pointer-events: none;"></div> <div class="position-absolute" style="bottom: -200px; left: -200px; width: 400px; height: 400px; background: rgba(var(--accent-rgb), 0.03); filter: blur(100px); border-radius: 50%; pointer-events: none;"></div> <div class="container position-relative z-3"> <div class="row g-4"> <!-- Column 1: Brand & About --> <div class="col-lg-4 col-md-6"> <div class="d-flex align-items-center mb-3"> <span class="brand-mark me-2" style="width: 42px; height: 42px;" aria-hidden="true"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg> </span> <span class="fs-4 fw-extrabold text-primary-custom" style="letter-spacing: -0.05em; font-family: 'JetBrains Mono', monospace;">NeshDevTech</span> </div> <p class="text-secondary-custom mb-4 pe-lg-4" style="font-size: 0.95rem; line-height: 1.6;">NeshDevTech is a professional technology firm passionate about creating innovative, high-performance web and mobile solutions.</p> <div class="d-flex gap-2"> </div> </div> <!-- Column 2: Navigation Links --> <div class="col-lg-2 col-md-6 col-6"> <h6 class="text-primary-custom fw-bold text-uppercase mb-3" style="font-size: 0.85rem; letter-spacing: 0.05em;">Quick Navigation</h6> <ul class="list-unstyled mb-0"> <li class="mb-2"><a href="https://neshdevtech.com" class="footer-link text-secondary-custom text-decoration-none d-inline-block">Home</a></li> <li class="mb-2"><a href="https://neshdevtech.com/projects" class="footer-link text-secondary-custom text-decoration-none d-inline-block">Projects</a></li> <li class="mb-2"><a href="https://neshdevtech.com/blog" class="footer-link text-secondary-custom text-decoration-none d-inline-block">Tech Blog</a></li> <li class="mb-2"><a href="https://neshdevtech.com/pricing" class="footer-link text-secondary-custom text-decoration-none d-inline-block">Get Quote</a></li> <li class="mb-2"><a href="https://neshdevtech.com/contact" class="footer-link text-secondary-custom text-decoration-none d-inline-block">Contact Me</a></li> </ul> </div> <!-- Column 3: Tech Stack & Services --> <div class="col-lg-3 col-md-6 col-6"> <h6 class="text-primary-custom fw-bold text-uppercase mb-3" style="font-size: 0.85rem; letter-spacing: 0.05em;">Core Services</h6> <ul class="list-unstyled mb-0"> <li class="text-secondary-custom mb-2 small"><i class="bi bi-rocket-takeoff text-primary-custom me-2"></i>Custom Laravel Apps</li> <li class="text-secondary-custom mb-2 small"><i class="bi bi-laptop text-primary-custom me-2"></i>SPA (React & Vue.js)</li> <li class="text-secondary-custom mb-2 small"><i class="bi bi-phone-vibrate text-primary-custom me-2"></i>Flutter Mobile Apps</li> <li class="text-secondary-custom mb-2 small"><i class="bi bi-search text-primary-custom me-2"></i>SEO & Performance Opt</li> </ul> </div> <!-- Column 4: Contact & Availability --> <div class="col-lg-3 col-md-6"> <h6 class="text-primary-custom fw-bold text-uppercase mb-3" style="font-size: 0.85rem; letter-spacing: 0.05em;">Get in Touch</h6> <ul class="list-unstyled mb-3"> <li class="text-secondary-custom mb-2 small d-flex align-items-center"> <i class="bi bi-geo-alt text-primary-custom me-2 fs-6"></i>Nairobi, Kenya </li> <li class="text-secondary-custom mb-2 small d-flex align-items-center"> <i class="bi bi-envelope-at text-primary-custom me-2 fs-6"></i> <a href="mailto:info@neshdevtech.com" class="text-secondary-custom text-decoration-none footer-link">info@neshdevtech.com</a> </li> </ul> <div class="bg-primary-custom-light border border-primary-custom border-opacity-10 p-3 rounded-3" style="border-radius: 12px !important;"> <small class="d-block fw-semibold text-primary-custom mb-1"><i class="bi bi-patch-check-fill me-1"></i>Available for Projects</small> <p class="mb-0 text-secondary-custom small" style="font-size: 0.8rem; line-height: 1.4;">Need a high-performing system? Let's discuss your project.</p> </div> </div> </div> <hr style="border-color: var(--border); opacity: 0.8; margin: 2.5rem 0 2rem;"> <div class="row align-items-center"> <div class="col-md-6 text-center text-md-start mb-2 mb-md-0"> <p class="mb-0 text-secondary-custom small">© 2026 NeshDevTech. All rights reserved.</p> </div> <div class="col-md-6 text-center text-md-end"> <p class="mb-0 text-secondary-custom small">Built with <i class="bi bi-heart-fill text-danger"></i></p> </div> </div> </div> </footer> <!-- Bootstrap JS --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <!-- Custom JavaScript --> <script> // Theme Management System class ThemeManager { constructor() { this.themeToggle = document.getElementById('themeToggle'); this.themeIcon = document.getElementById('theme-icon'); this.mobileThemeToggle = document.getElementById('mobileThemeToggle'); this.mobileThemeIcon = document.getElementById('mobile-theme-icon'); this.init(); } init() { // Get current theme (already applied in head) const currentTheme = document.documentElement.getAttribute('data-theme') || 'light'; // Update icons to match current theme this.updateIcon(currentTheme); // Listen for desktop theme toggle clicks if (this.themeToggle) { this.themeToggle.addEventListener('click', (e) => { e.preventDefault(); this.toggleTheme(); }); } // Listen for mobile theme toggle clicks if (this.mobileThemeToggle) { this.mobileThemeToggle.addEventListener('click', (e) => { e.preventDefault(); this.toggleTheme(); }); } // Listen for system theme changes window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { if (!localStorage.getItem('theme')) { this.setTheme(e.matches ? 'dark' : 'light'); } }); } setTheme(theme) { document.documentElement.setAttribute('data-theme', theme); document.documentElement.setAttribute('data-bs-theme', theme); if (theme === 'dark') { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } localStorage.setItem('theme', theme); this.updateThemeColor(theme); this.updateIcon(theme); } updateThemeColor(theme) { document.querySelectorAll('meta[name="theme-color"]').forEach((meta) => { meta.setAttribute('content', theme === 'dark' ? '#1c2028' : '#e4e8f0'); }); } toggleTheme() { const currentTheme = document.documentElement.getAttribute('data-theme'); const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; this.setTheme(newTheme); // Close mobile menu if open const navbarCollapse = document.querySelector('.navbar-collapse'); if (navbarCollapse && navbarCollapse.classList.contains('show') && window.innerWidth < 992) { setTimeout(() => { const bsCollapse = new bootstrap.Collapse(navbarCollapse, { toggle: false }); bsCollapse.hide(); }, 150); } } updateIcon(theme) { const iconClass = theme === 'dark' ? 'bi bi-sun-fill' : 'bi bi-moon-fill'; // Update desktop icon if (this.themeIcon) { this.themeIcon.className = iconClass; } // Update mobile icon if (this.mobileThemeIcon) { this.mobileThemeIcon.className = iconClass; } } } // Initialize theme manager when DOM is loaded document.addEventListener('DOMContentLoaded', () => { new ThemeManager(); // Initialize navbar live clock (function initNavClock(){ const clockEls = Array.from(document.querySelectorAll('[data-clock]')); if (!clockEls.length) return; const locale = navigator.language || 'en-US'; // Detect 12/24h by locale: use resolvedOptions hourCycle when available const dtf = new Intl.DateTimeFormat(locale, { hour: 'numeric' }); const uses12h = (dtf.resolvedOptions().hour12 === true); const fmt = new Intl.DateTimeFormat(locale, { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: uses12h }); const update = () => { const now = new Date(); const text = fmt.format(now); clockEls.forEach(el => { el.textContent = text; }); }; update(); setInterval(update, 1000); })(); }); // Smooth scrolling for anchor links document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function (e) { e.preventDefault(); const target = document.querySelector(this.getAttribute('href')); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); }); // Navbar background on scroll window.addEventListener('scroll', () => { const navbar = document.querySelector('.navbar'); if (window.scrollY > 50) { navbar.style.backdropFilter = 'blur(20px)'; navbar.style.backgroundColor = 'rgba(var(--background-rgb), 0.8)'; } else { navbar.style.backdropFilter = 'blur(10px)'; navbar.style.backgroundColor = 'var(--background)'; } }); // Add fade-in animation to elements when they come into view const observerOptions = { threshold: 0.1, rootMargin: '0px 0px -50px 0px' }; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('animate-fade-in-up'); } }); }, observerOptions); // Observe elements with animation class document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.animate-on-scroll').forEach(el => { observer.observe(el); }); }); // Enhanced mobile menu functionality document.addEventListener('DOMContentLoaded', function() { const navbarToggler = document.querySelector('.navbar-toggler'); const navbarCollapse = document.querySelector('.navbar-collapse'); const navLinks = document.querySelectorAll('.navbar-nav .nav-link'); const socialLinks = document.querySelectorAll('.navbar-nav .d-flex a'); let isToggling = false; // Prevent rapid clicking // Enhanced menu close function function closeMenu() { if (navbarCollapse && navbarCollapse.classList.contains('show')) { // Use Bootstrap's collapse API for smooth transition const bsCollapse = new bootstrap.Collapse(navbarCollapse, { toggle: false }); bsCollapse.hide(); } // Reset hamburger animation and aria-expanded if (navbarToggler) { navbarToggler.classList.remove('active'); navbarToggler.setAttribute('aria-expanded', 'false'); } } // Enhanced hamburger toggle functionality if (navbarToggler) { navbarToggler.addEventListener('click', function(e) { e.preventDefault(); // Prevent rapid clicking if (isToggling) return; isToggling = true; const isMenuOpen = navbarCollapse.classList.contains('show'); if (isMenuOpen) { closeMenu(); } else { // Open menu const bsCollapse = new bootstrap.Collapse(navbarCollapse, { toggle: false }); bsCollapse.show(); navbarToggler.setAttribute('aria-expanded', 'true'); } // Reset toggle flag after animation setTimeout(() => { isToggling = false; }, 350); }); } // Close menu when clicking outside document.addEventListener('click', function(event) { const isClickInsideNav = navbarCollapse.contains(event.target); const isToggler = navbarToggler.contains(event.target); const isMenuOpen = navbarCollapse.classList.contains('show'); if (!isClickInsideNav && !isToggler && isMenuOpen && window.innerWidth < 992) { closeMenu(); } }); // Close menu when clicking on navigation links (mobile) navLinks.forEach(function(link) { link.addEventListener('click', function(e) { const isMenuOpen = navbarCollapse.classList.contains('show'); if (isMenuOpen && window.innerWidth < 992 && !this.classList.contains('dropdown-toggle')) { // Add a small delay to let the link navigation start setTimeout(() => { closeMenu(); }, 100); } }); }); // Close menu when clicking on social links (mobile) socialLinks.forEach(function(link) { link.addEventListener('click', function() { if (window.innerWidth < 992) { setTimeout(() => { closeMenu(); }, 100); } }); }); // Escape key closes menu document.addEventListener('keydown', function(event) { if (event.key === 'Escape' && window.innerWidth < 992) { closeMenu(); } }); // Handle window resize to close menu when switching to desktop window.addEventListener('resize', function() { if (window.innerWidth >= 992) { closeMenu(); } }); // Theme toggle functionality is handled by ThemeManager class // No separate event listener needed here // Prevent menu items from being too small on touch devices if ('ontouchstart' in window) { navLinks.forEach(function(link) { link.style.minHeight = '44px'; link.style.display = 'flex'; link.style.alignItems = 'center'; }); } // Add smooth scrolling for anchor links within pages navLinks.forEach(function(link) { const href = link.getAttribute('href'); if (href && href.startsWith('#')) { link.addEventListener('click', function(e) { e.preventDefault(); const target = document.querySelector(href); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } closeMenu(); }); } }); }); </script> <script> // Sticky navbar scroll enhancement (desktop only) (function () { const navbar = document.querySelector('.navbar'); if (!navbar) return; let ticking = false; const SCROLL_THRESHOLD = 20; // px function updateNavbar() { if (window.innerWidth >= 992) { if (window.scrollY > SCROLL_THRESHOLD) { navbar.classList.add('scrolled'); } else { navbar.classList.remove('scrolled'); } } else { // Always remove on mobile so it doesn't affect mobile look navbar.classList.remove('scrolled'); } ticking = false; } window.addEventListener('scroll', function () { if (!ticking) { requestAnimationFrame(updateNavbar); ticking = true; } }, { passive: true }); window.addEventListener('resize', updateNavbar, { passive: true }); // Run once on page load in case page is already scrolled (e.g. back-navigation) updateNavbar(); })(); </script> <script> // Lightweight toast badge notifications (function(){ function ensureContainer(){ let c = document.getElementById('toast-badge-container'); if(!c){ c = document.createElement('div'); c.id = 'toast-badge-container'; c.setAttribute('aria-live','polite'); c.setAttribute('aria-atomic','true'); document.body.appendChild(c); } return c; } function iconFor(type){ switch(type){ case 'success': return '<i class="bi bi-check-circle"></i>'; case 'warning': return '<i class="bi bi-exclamation-triangle"></i>'; case 'error': return '<i class="bi bi-x-circle"></i>'; default: return '<i class="bi bi-info-circle"></i>'; } } window.notifyBadge = function(message, opts={}){ const { type = 'success', timeout = 2500 } = opts; const container = ensureContainer(); const el = document.createElement('div'); el.className = 'toast-badge ' + type; el.setAttribute('role','status'); el.innerHTML = '<span class="icon">'+iconFor(type)+'</span><span>'+message+'</span>'; container.appendChild(el); requestAnimationFrame(()=>{ el.classList.add('show'); }); const remove = () => { el.classList.remove('show'); setTimeout(()=> el.remove(), 200); }; setTimeout(remove, timeout); return { remove }; }; // Global copy helper used by inline buttons window.copyToClipboard = function(url){ try { navigator.clipboard.writeText(url).then(()=>{ // Update the clicked button, if available let btn = document.activeElement && document.activeElement.tagName === 'BUTTON' ? document.activeElement : (window.event && window.event.target && window.event.target.closest ? window.event.target.closest('button') : null); if (btn){ const original = btn.innerHTML; btn.innerHTML = '<i class="fas fa-check me-1"></i> Copied!'; btn.classList.remove('btn-outline-secondary'); btn.classList.add('btn-success'); setTimeout(()=>{ btn.innerHTML = original; btn.classList.remove('btn-success'); btn.classList.add('btn-outline-secondary'); }, 2000); } notifyBadge('Link copied to clipboard', { type: 'success' }); }).catch((err)=>{ console.error('Clipboard error:', err); notifyBadge('Could not copy link', { type: 'error' }); }); } catch (e) { console.error(e); notifyBadge('Copy not supported', { type: 'error' }); } }; })(); </script> <a href="https://wa.me/254768033943?text=Hi%2C+I+visited+your+website+and+would+like+to+inquire+about+your+services." class="whatsapp-btn-float" target="_blank" rel="noopener noreferrer" aria-label="Chat on WhatsApp"> <i class="fab fa-whatsapp"></i> <span class="whatsapp-tooltip">Chat with me</span> </a> <style> .whatsapp-btn-float { position: fixed; bottom: 24px; right: 24px; z-index: 1050; display: flex; align-items: center; justify-content: center; background-color: #25d366; color: #fff !important; width: 60px; height: 60px; border-radius: 50%; box-shadow: 0 4px 16px rgba(37, 211, 102, 0.45); text-decoration: none; transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); } .whatsapp-btn-float i { font-size: 32px; transition: transform 0.3s ease; } .whatsapp-btn-float:hover { background-color: #128c7e; transform: scale(1.1); box-shadow: 0 6px 20px rgba(18, 140, 126, 0.6); } .whatsapp-btn-float:hover i { transform: rotate(10deg); } /* Pulsing ring animation */ .whatsapp-btn-float::before { content: ''; position: absolute; width: 100%; height: 100%; border-radius: 50%; background-color: #25d366; opacity: 0.7; z-index: -1; animation: whatsapp-pulse 2s infinite; } @keyframes whatsapp-pulse { 0% { transform: scale(1); opacity: 0.7; } 100% { transform: scale(1.6); opacity: 0; } } /* Tooltip style utilizing site-wide theme colors */ .whatsapp-btn-float .whatsapp-tooltip { position: absolute; right: 76px; background-color: var(--surface); color: var(--text-primary); border: 1px solid var(--border); padding: 8px 14px; border-radius: 20px; font-size: 14px; font-weight: 600; white-space: nowrap; opacity: 0; visibility: hidden; transform: translateX(10px); transition: all 0.3s ease; box-shadow: var(--shadow); pointer-events: none; } .whatsapp-btn-float:hover .whatsapp-tooltip { opacity: 1; visibility: visible; transform: translateX(0); } /* Responsive Design */ @media (max-width: 768px) { .whatsapp-btn-float { bottom: 16px; right: 16px; width: 50px; height: 50px; } .whatsapp-btn-float i { font-size: 26px; } .whatsapp-btn-float .whatsapp-tooltip { display: none; /* Hide tooltip on small screens to prevent overlap issues */ } } </style> <!-- Mobile More Menu (Offcanvas Bottom Sheet) - Auth actions only --> <style> /* ══════════════════════════════════════════════════════════════ NEUMORPHISM REVAMP (front-end only) Soft-extruded UI: same-tone surfaces with dual light/dark shadows. This block sits at the end of the body so it layers over every page's own styles without touching page markup. ══════════════════════════════════════════════════════════════ */ /* ── Buttons: extruded, press-in on click ─────────────────────── */ .btn { box-shadow: var(--shadow) !important; transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1) !important; } .btn:hover { transform: translateY(-2px) !important; box-shadow: var(--shadow-md) !important; } .btn:active:not(.btn-link), .btn.show { transform: translateY(1px) scale(0.99) !important; box-shadow: var(--shadow-inset) !important; } .btn-primary-custom, .btn-primary, .btn-success { background: var(--primary-color) !important; border-color: var(--primary-color) !important; color: #fff !important; box-shadow: var(--shadow) !important; } .btn-primary-custom:hover, .btn-primary:hover, .btn-success:hover { background: var(--primary-dark) !important; border-color: var(--primary-dark) !important; color: #fff !important; box-shadow: var(--shadow-md) !important; } .btn-outline-primary-custom, .btn-outline-primary, .btn-outline-success, .btn-outline-secondary, .btn-outline-danger, .btn-outline-dark, .btn-light { background-color: var(--surface) !important; border-color: transparent !important; color: var(--text-primary) !important; box-shadow: var(--shadow) !important; } .btn-outline-primary-custom:hover, .btn-outline-primary:hover { background-color: var(--primary-color) !important; border-color: var(--primary-color) !important; color: #fff !important; box-shadow: var(--shadow-md) !important; } .btn-outline-success:hover { background-color: var(--primary-color) !important; border-color: var(--primary-color) !important; color: #fff !important; box-shadow: var(--shadow-md) !important; } .btn-outline-secondary:hover { background-color: var(--text-secondary) !important; border-color: var(--text-secondary) !important; color: #fff !important; box-shadow: var(--shadow-md) !important; } .btn-outline-danger:hover { background-color: #ef4444 !important; border-color: #ef4444 !important; color: #fff !important; box-shadow: var(--shadow-md) !important; } .btn-outline-dark:hover, .btn-light:hover { background-color: var(--text-secondary) !important; border-color: var(--text-secondary) !important; color: var(--background) !important; box-shadow: var(--shadow-md) !important; } .btn-dark { background-color: var(--text-primary) !important; border-color: var(--text-primary) !important; color: var(--background) !important; box-shadow: var(--shadow) !important; } /* ── Forms: pressed-in surfaces ────────────────────────────────── */ .form-control, .form-select { background-color: var(--surface) !important; border: 1px solid transparent !important; color: var(--text-primary) !important; border-radius: 14px !important; box-shadow: var(--shadow-inset) !important; transition: box-shadow 0.3s ease, background-color 0.25s ease !important; } .form-control:focus, .form-select:focus { background-color: var(--surface) !important; border-color: transparent !important; color: var(--text-primary) !important; box-shadow: var(--shadow-inset), 0 0 0 4px rgba(var(--primary-rgb), 0.15) !important; } .form-control::placeholder { color: var(--text-muted) !important; } .input-group-text { background-color: var(--surface-alt) !important; border: 1px solid transparent !important; color: var(--text-secondary) !important; box-shadow: var(--shadow-inset-sm) !important; } /* ── Navbar: floating neumorphic pill (tablet & desktop) ──────── */ .navbar { background-color: rgba(var(--background-rgb), 0.9) !important; border-bottom: 1px solid transparent !important; backdrop-filter: blur(16px) !important; -webkit-backdrop-filter: blur(16px) !important; box-shadow: var(--shadow-md) !important; } [data-theme="dark"] .navbar { box-shadow: var(--shadow-md) !important; } @media (min-width: 768px) { .navbar { top: 14px !important; left: 18px !important; right: 18px !important; width: auto !important; border-radius: 20px !important; padding: 0.6rem 0 !important; box-shadow: var(--shadow-lg) !important; } [data-theme="dark"] .navbar { box-shadow: var(--shadow-lg) !important; } .navbar.scrolled { background-color: rgba(var(--background-rgb), 0.95) !important; border-bottom-color: transparent !important; padding: 0.5rem 0 !important; box-shadow: var(--shadow-lg) !important; } [data-theme="dark"] .navbar.scrolled { background-color: rgba(28, 32, 40, 0.95) !important; box-shadow: var(--shadow-lg) !important; } body { padding-top: 96px !important; } .reading-progress-container { top: 88px !important; } } /* ── Tablet refinements (768px - 991.98px) ────────────────────── */ @media (min-width: 768px) and (max-width: 991.98px) { /* Hamburger dropdown panel blends with the floating pill */ .navbar-collapse { border-radius: 20px !important; border-color: transparent !important; } body { padding-bottom: 0 !important; } .whatsapp-btn-float { bottom: 24px !important; } /* Hero: tighten the inline 80px top padding that assumed a full-height bar, so tablets don't get excessive empty space */ .hero-section-bg { padding-top: 40px !important; } } /* ── Neumorphic brand icon mark ────────────────────────────────── */ .brand-mark { width: 40px; height: 40px; flex-shrink: 0; border-radius: 13px; background-color: var(--surface) !important; color: var(--primary-color) !important; display: inline-flex; align-items: center; justify-content: center; box-shadow: var(--shadow) !important; transition: box-shadow 0.3s cubic-bezier(0.16, 1, 0.3, 1), transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); } .brand-mark svg { width: 21px; height: 21px; } a:hover .brand-mark { box-shadow: var(--shadow-inset-sm) !important; transform: translateY(1px); } .mobile-logo .brand-mark { width: 34px; height: 34px; border-radius: 11px; } .mobile-logo .brand-mark svg { width: 17px; height: 17px; } /* ── Uploaded brand logo image ─────────────────────────────────── */ .brand-img { height: 38px; width: auto; max-width: 160px; object-fit: contain; flex-shrink: 0; } .brand-img-sm { height: 28px; max-width: 120px; } .brand-img-footer { height: 44px; max-width: 180px; } /* ── Mobile bars: floating neumorphic pills (phones) ──────────── */ @media (max-width: 767.98px) { .mobile-top-bar { top: 8px !important; left: 12px !important; right: 12px !important; height: 52px !important; border-radius: 16px !important; border-bottom: none !important; background-color: rgba(var(--surface-rgb), 0.92) !important; box-shadow: var(--shadow-md) !important; } .mobile-bottom-bar { left: 12px !important; right: 12px !important; bottom: 12px !important; height: 58px !important; border-radius: 18px !important; border-top: none !important; background-color: rgba(var(--surface-rgb), 0.92) !important; box-shadow: var(--shadow-md) !important; } body { padding-top: 74px !important; padding-bottom: 86px !important; } .whatsapp-btn-float { bottom: 88px !important; } .reading-progress-container { top: 70px !important; } } /* ── Nav pill widgets: pressed-in ──────────────────────────────── */ .theme-toggle, .mobile-theme-toggle, .nav-clock { background-color: var(--surface) !important; border-color: transparent !important; box-shadow: var(--shadow-inset-sm) !important; } /* ── Icon chips, markers & circles: extruded ───────────────────── */ .icon-wrap, .icon-wrapper, .expertise-card-icon, .timeline-marker, .mobile-more-icon-wrapper, .social-circle, .value-icon, .skill-icon-box { box-shadow: var(--shadow) !important; border-color: transparent !important; } /* ── Panels: extruded ──────────────────────────────────────────── */ .accordion-item { border-color: transparent !important; box-shadow: var(--shadow) !important; } .timeline-content { background: var(--surface) !important; border-radius: 12px !important; padding: 1rem 1.25rem !important; box-shadow: var(--shadow) !important; } .feature-item { background: var(--surface) !important; border-color: transparent !important; box-shadow: var(--shadow) !important; } .feature-item:hover { transform: translateY(-2px) !important; box-shadow: var(--shadow-md) !important; } .progress { background-color: var(--surface-alt) !important; box-shadow: var(--shadow-inset-sm) !important; } /* Card hover: consistent soft neu shadow */ .card-hover:hover, .project-card:hover, .blog-card:hover, .post-card:hover, .news-card:hover, .testimonial-card:hover, .hover-lift:hover, .stats-item:hover { box-shadow: var(--shadow-lg) !important; } </style> <script> document.addEventListener('DOMContentLoaded', function() { const readerCard = document.getElementById('reader-card'); if (!readerCard) return; // Settings Key constants const THEME_KEY = 'reader_mode_theme'; const FONT_KEY = 'reader_mode_font'; const SIZE_KEY = 'reader_mode_size'; // Available Options const themes = ['light', 'sepia', 'dark', 'oled']; const fonts = ['serif', 'sans']; const sizes = ['xs', 'sm', 'md', 'lg', 'xl']; // Default Configs const siteTheme = document.documentElement.getAttribute('data-theme') || 'light'; const defaultTheme = siteTheme === 'dark' ? 'dark' : 'light'; let currentTheme = localStorage.getItem(THEME_KEY) || defaultTheme; let currentFont = localStorage.getItem(FONT_KEY) || 'serif'; let currentSize = localStorage.getItem(SIZE_KEY) || 'md'; // Apply initial config applySettings(); // Register Action Listeners (Toolbar) document.getElementById('btn-font-serif').addEventListener('click', () => setFont('serif')); document.getElementById('btn-font-sans').addEventListener('click', () => setFont('sans')); document.getElementById('btn-size-decrease').addEventListener('click', () => adjustSize(-1)); document.getElementById('btn-size-increase').addEventListener('click', () => adjustSize(1)); document.getElementById('theme-light').addEventListener('click', () => setTheme('light')); document.getElementById('theme-sepia').addEventListener('click', () => setTheme('sepia')); document.getElementById('theme-dark').addEventListener('click', () => setTheme('dark')); document.getElementById('theme-oled').addEventListener('click', () => setTheme('oled')); // Register Action Listeners (Sidebar widgets, if present) const sidebarSerif = document.getElementById('sidebar-font-serif'); const sidebarSans = document.getElementById('sidebar-font-sans'); const sidebarSizeDec = document.getElementById('sidebar-size-dec'); const sidebarSizeInc = document.getElementById('sidebar-size-inc'); if (sidebarSerif) sidebarSerif.addEventListener('click', () => setFont('serif')); if (sidebarSans) sidebarSans.addEventListener('click', () => setFont('sans')); if (sidebarSizeDec) sidebarSizeDec.addEventListener('click', () => adjustSize(-1)); if (sidebarSizeInc) sidebarSizeInc.addEventListener('click', () => adjustSize(1)); // Action Core Functions function setTheme(theme) { currentTheme = theme; localStorage.setItem(THEME_KEY, theme); applySettings(); } function setFont(font) { currentFont = font; localStorage.setItem(FONT_KEY, font); applySettings(); } function adjustSize(delta) { let index = sizes.indexOf(currentSize); index = Math.max(0, Math.min(sizes.length - 1, index + delta)); currentSize = sizes[index]; localStorage.setItem(SIZE_KEY, currentSize); applySettings(); } function applySettings() { // 1. Reset theme classes and apply selected themes.forEach(t => readerCard.classList.remove('reader-theme-' + t)); readerCard.classList.add('reader-theme-' + currentTheme); // Update Theme circles highlight themes.forEach(t => { const circle = document.getElementById('theme-' + t); if (circle) { if (t === currentTheme) { circle.classList.add('active'); } else { circle.classList.remove('active'); } } }); // 2. Reset Font classes and apply selected fonts.forEach(f => readerCard.classList.remove('reader-font-' + f)); readerCard.classList.add('reader-font-' + currentFont); // Highlight Font buttons const serifBtns = [document.getElementById('btn-font-serif'), document.getElementById('sidebar-font-serif')]; const sansBtns = [document.getElementById('btn-font-sans'), document.getElementById('sidebar-font-sans')]; serifBtns.forEach(btn => { if (btn) { if (currentFont === 'serif') { btn.classList.add('btn-primary-custom'); btn.classList.remove('btn-light'); } else { btn.classList.add('btn-light'); btn.classList.remove('btn-primary-custom'); } } }); sansBtns.forEach(btn => { if (btn) { if (currentFont === 'sans') { btn.classList.add('btn-primary-custom'); btn.classList.remove('btn-light'); } else { btn.classList.add('btn-light'); btn.classList.remove('btn-primary-custom'); } } }); // 3. Reset Sizing classes and apply selected sizes.forEach(s => readerCard.classList.remove('reader-size-' + s)); readerCard.classList.add('reader-size-' + currentSize); } // Reading Scroll progress calculation const progressBar = document.getElementById('reading-progress-bar'); window.addEventListener('scroll', () => { const documentHeight = document.documentElement.scrollHeight - window.innerHeight; if (documentHeight > 0) { const scrollProgress = (window.pageYOffset / documentHeight) * 100; progressBar.style.width = scrollProgress + '%'; } }); }); </script> <script> // ══════════════════════════════════════════════════════════════ // PWA: Service Worker registration + install prompt // ══════════════════════════════════════════════════════════════ (function () { const PWA_VERSION = '20260805-1'; // ---- Service worker ------------------------------------ if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js?v=' + PWA_VERSION).then((reg) => { // Apply updates (new deploy) on the next load without disruption reg.addEventListener('updatefound', () => { const newWorker = reg.installing; if (!newWorker) return; newWorker.addEventListener('statechange', () => { if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { newWorker.postMessage({ type: 'SKIP_WAITING' }); } }); }); }).catch(() => {}); }); } // ---- Install prompt -------------------------------------- let deferredPrompt = null; const bannerId = 'pwa-install-banner'; const DISMISS_KEY = 'nesh-install-dismissed'; function showInstallBanner() { if (localStorage.getItem(DISMISS_KEY) || document.getElementById(bannerId)) return; const banner = document.createElement('div'); banner.id = bannerId; banner.setAttribute('role', 'dialog'); banner.setAttribute('aria-label', 'Install app'); banner.innerHTML = '<div class="pwa-install-content">' + '<span class="pwa-install-icon">' + '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>' + '</span>' + '<div class="pwa-install-text">' + '<strong>Install NeshDevTech</strong>' + '<span>Get the app on your device</span>' + '</div>' + '<button type="button" class="pwa-install-btn" id="pwa-install-btn">Install</button>' + '<button type="button" class="pwa-install-close" id="pwa-install-close" aria-label="Dismiss">×</button>' + '</div>'; document.body.appendChild(banner); document.getElementById('pwa-install-btn').addEventListener('click', () => { if (!deferredPrompt) return; deferredPrompt.prompt(); deferredPrompt.userChoice.finally(() => { deferredPrompt = null; hideInstallBanner(); }); }); document.getElementById('pwa-install-close').addEventListener('click', () => { localStorage.setItem(DISMISS_KEY, '1'); hideInstallBanner(); }); requestAnimationFrame(() => banner.classList.add('show')); } function hideInstallBanner() { const banner = document.getElementById(bannerId); if (banner) { banner.classList.remove('show'); setTimeout(() => banner.remove(), 250); } } window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredPrompt = e; showInstallBanner(); }); window.addEventListener('appinstalled', () => { deferredPrompt = null; hideInstallBanner(); localStorage.setItem(DISMISS_KEY, '1'); }); })(); </script> <style> .pwa-install-content { display: flex; align-items: center; gap: 12px; padding: 14px 18px; } #pwa-install-banner { position: fixed; left: 50%; transform: translate(-50%, 20px); bottom: 84px; z-index: 1070; background: var(--surface); color: var(--text-primary); border: 1px solid var(--border); border-radius: 18px; box-shadow: var(--shadow-lg); opacity: 0; transition: opacity .25s cubic-bezier(.16, 1, .3, 1), transform .25s cubic-bezier(.16, 1, .3, 1); width: min(420px, calc(100vw - 32px)); } #pwa-install-banner.show { opacity: 1; transform: translate(-50%, 0); } .pwa-install-icon { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; flex: 0 0 40px; border-radius: 12px; background: rgba(var(--primary-rgb), 0.1); border: 1px solid rgba(var(--primary-rgb), 0.2); color: var(--primary-color); } .pwa-install-icon svg { width: 20px; height: 20px; } .pwa-install-text { display: flex; flex-direction: column; flex: 1; min-width: 0; } .pwa-install-text strong { font-size: 0.92rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .pwa-install-text span { font-size: 0.78rem; color: var(--text-secondary); } .pwa-install-btn { background: var(--primary-color); border: none; color: #fff; font-weight: 600; font-size: 0.85rem; padding: 8px 18px; border-radius: 9999px; cursor: pointer; white-space: nowrap; box-shadow: 0 4px 12px rgba(var(--primary-rgb), 0.25); transition: all .2s ease; } .pwa-install-btn:hover { background: var(--primary-dark); transform: translateY(-1px); } .pwa-install-close { background: transparent; border: none; color: var(--text-muted); font-size: 1.4rem; line-height: 1; cursor: pointer; padding: 2px 6px; border-radius: 50%; transition: background-color .2s ease, color .2s ease; } .pwa-install-close:hover { background: var(--surface-alt); color: var(--text-primary); } @media (min-width: 992px) { #pwa-install-banner { bottom: 24px; } } </style> </div><!-- /.page-wrapper --> </body> </html>