{
  "categories": {
    "convert": "Convert a document",
    "pdf": "PDF page tools",
    "code": "Code converters",
    "image": "Images",
    "svg": "SVG",
    "audio": "Audio",
    "other": "Workflow"
  },
  "tools": [
    {
      "slug": "acrobat-for-teachers",
      "name": "Acrobat for teachers / test-prep creators",
      "category": "other",
      "one_liner": "Chat flagship #2",
      "replaces": "Acrobat + Express generic document tools",
      "primitive": "the teacher/test-prep workflow as the unit",
      "status": "planned",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "ai_resume_builder",
      "name": "AI resume builder",
      "category": "other",
      "one_liner": "[mt4] First draft returned 2026-07-10; audited by irp-o.",
      "replaces": "Adobe/Express resume",
      "primitive": "schema-first resume + render",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "audio-album-art",
      "name": "Extract album art",
      "category": "audio",
      "one_liner": "Pull the embedded cover art out of any audio file at full resolution.",
      "replaces": "Screenshotting cover art out of a music player",
      "primitive": "Pull the embedded artwork stream directly (mutagen first, ffmpeg fallback) at original resolution",
      "status": "done",
      "runner": "audiotags",
      "script": null,
      "tests": "7/9",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Extract album art at full size in its real format",
      "search_description": "Pull every picture embedded in an audio file out at full resolution, each saved under the format it is actually stored in. Runs locally, nothing is uploaded.",
      "search_intro": "An audio file can carry more than one embedded picture, and they are not all JPEG. A tool that returns a single file named for the format it expected will hand back PNG bytes under a name ending in jpg, and will drop the second picture without saying so. This page reads the file first and lists what is in it: every attached picture, its format, its pixel size and the label the file gives it. Each one is then copied out under an extension that matches the bytes, with no re-encoding and no resizing.",
      "how_it_works": [
        "Choose an audio file. It is read in this tab and every embedded picture is listed with its format and pixel size.",
        "Review the pictures, then extract them. Each is copied out at its stored resolution.",
        "Download them individually. A file with no embedded picture says so instead of producing an empty result."
      ],
      "use_cases": [
        "Recover cover art from a music library at the resolution the files actually hold.",
        "Check whether a release carries a back cover or a booklet page as well as a front cover.",
        "Get artwork out of a podcast or audiobook file before re-tagging it."
      ],
      "limits": [
        "Only pictures embedded in the file are reachable. Artwork stored beside the file as a separate image is not part of the container.",
        "Pictures are copied rather than converted, so the format is whatever the file holds and no upscaling happens.",
        "The role a picture claims, such as front cover against back cover, is not exposed by the container's generic interface and is not reported."
      ],
      "faq": [
        {
          "q": "Is the file uploaded?",
          "a": "No. The file is read and the pictures are copied out in this tab. The audio engine is fetched once and cached."
        },
        {
          "q": "Why is the extracted file sometimes a PNG?",
          "a": "Because that is what the file holds. The extension follows the actual stored format rather than being fixed in advance."
        },
        {
          "q": "What happens with more than one picture?",
          "a": "Every attached picture is extracted, numbered in the order the file stores them, each with its own format and size."
        }
      ],
      "related": [
        "audio-metadata",
        "audio-analyze",
        "image-optimizer"
      ]
    },
    {
      "slug": "audio-analyze",
      "name": "Analyze audio",
      "category": "audio",
      "one_liner": "Measure integrated loudness, loudness range and true peak against a platform target, and draw the spectrogram and waveform the measurement came from.",
      "replaces": "Analyzers that make you open a full DAW to see a waveform",
      "primitive": "One command for waveform, spectrogram, BPM, key, LUFS and peak",
      "status": "done",
      "runner": "audioanalyze",
      "script": null,
      "tests": "40/40",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Measure audio loudness and see the spectrum in the browser",
      "search_description": "Read integrated loudness in LUFS, loudness range and true peak from a recording, compared against the level your platform normalises to, beside a spectrogram and a waveform drawn from the same file. Runs locally, nothing is uploaded.",
      "search_intro": "Every consumer platform turns a track up or down at playback until its measured loudness matches a fixed level, so the number that decides how loud a listener hears a recording is calculated after upload and is almost never shown to whoever made it. That number is integrated loudness, expressed in LUFS, and it comes from an algorithm standardised as ITU-R BS.1770 that weights the signal for human hearing and ignores the quiet passages instead of averaging them in. A peak meter cannot answer it: two files can peak at the identical value and sit several LUFS apart, and the quieter one will be turned up while the louder one is turned down. This page measures the loudness figure, the loudness range that says how far the loud and quiet passages sit apart, and the true peak, which is measured between samples rather than only at them and is therefore the one that predicts whether a lossy encoder will clip. Each figure is stated against the target you pick, in text, with the difference in LU. Beside them the same file is drawn twice: a spectrogram on a logarithmic frequency scale, so the musical range is spread evenly rather than squeezed into the bottom of the picture, and a waveform that plots the loudest sample in each column and separates the channels, so a short transient and a lopsided mix are both visible instead of being averaged away.",
      "how_it_works": [
        "Choose one audio file. It is decoded in this tab and its length, sample rate and channel count are read before anything is measured.",
        "Pick the level your release is aimed at, and the way the pictures should be drawn: logarithmic or linear frequency, peak or average waveform sampling, channels together or apart, and the image size.",
        "Analyze. Levels are measured in a pass of their own, ahead of the drawing, so a picture that fails to render still leaves the numbers on the page.",
        "Read the verdict, the loudness table and the per-channel levels, then download either picture or the whole measurement as JSON."
      ],
      "use_cases": [
        "Find out whether a master will be turned down at playback, and by how much, before it is uploaded anywhere.",
        "Check a true peak against the ceiling a lossy encoder needs, which a sample peak meter will read as safe when it is not.",
        "See whether one channel is quieter than the other, or whether a recording has a noise floor or a constant offset, without opening a full editor.",
        "Look at what actually occupies the top of the spectrum in a recording, on an axis where the musical range is legible."
      ],
      "limits": [
        "Tempo in beats per minute and musical key are not measured. Both need a library this browser build leaves out, and a guessed answer is worse than no answer.",
        "The tolerance band used to call a level at target is this tool's own stated band, not a quoted requirement from any standard or platform.",
        "Loudness is measured over the whole file. A short measurement window and a moving meter are a different job from a delivery check.",
        "The spectrogram's requested size is the plot area, and the legend is drawn outside it, so the produced picture is larger than the size requested. The size actually produced is read back from the image and reported with it."
      ],
      "faq": [
        [
          "Is the file uploaded?",
          "No. Decoding, measurement, drawing and export all happen in this browser tab."
        ],
        [
          "Why does the loudness number differ from my peak meter?",
          "They measure different things. A peak meter reports the highest sample; integrated loudness weights the signal for hearing, gates out the quiet passages and reports one figure for the whole programme. Two files with identical peaks can differ by several LUFS."
        ],
        [
          "What is the difference between true peak and peak level?",
          "Peak level is the loudest sample present. True peak reconstructs the waveform between samples and reports the highest point it reaches there, which can be higher and is what a lossy encoder has to cope with."
        ],
        [
          "Which target should I pick?",
          "Pick the one your release is going to. The page states what each level corresponds to, and reports the difference in LU either way, so the measurement is useful whichever target is selected."
        ]
      ],
      "related": [
        "audio-normalize",
        "audio-volume",
        "audio-dynamics"
      ]
    },
    {
      "slug": "audio-censor",
      "name": "Beep censor",
      "category": "audio",
      "one_liner": "Beep out listed time ranges, for profanity or anything you need gone.",
      "replaces": "Loading a full DAW to bleep three words in a podcast",
      "primitive": "Time-range list in, beeped copy out; tone frequency and padding are explicit flags",
      "status": "done",
      "runner": "audioregions",
      "script": null,
      "tests": "7/7",
      "from": null,
      "to": null,
      "public": false,
      "search_title": "Beep out parts of an audio recording",
      "search_description": "Mark the passages to censor on the waveform and replace each with a generated tone. Nothing is uploaded.",
      "search_intro": "Censoring is a review job before it is an encoding job: the exact passage matters, and a range typed from memory is usually wrong by a syllable. This workspace marks each passage on the waveform, plays it back, and replaces it with a tone generated at the frequency, level and fade chosen here. The surrounding audio is re-cut around the marks and rejoined, and a mark that runs past the end of the recording is refused rather than quietly dropping the tail.",
      "how_it_works": [
        "Choose an audio file and review the waveform.",
        "Add a beep region for each passage, then drag or type its exact bounds.",
        "Use play with regions skipped to hear the recording without the censored passages.",
        "Set the tone frequency, level and fade length.",
        "Run to build the censored file locally and download it."
      ],
      "use_cases": [
        "Removing names or numbers from a recorded call before sharing it.",
        "Bleeping language in a podcast episode.",
        "Masking a passage in evidence audio without cutting its running time."
      ],
      "limits": [
        "The tone is synthesized as a sine wave at the chosen frequency, so the censored passage keeps its original length rather than being cut out.",
        "Regions are rebuilt through a lossless intermediate and re-encoded once at the end.",
        "A region that starts at or past the end of the recording is refused, and a region ending exactly at the end leaves no trailing segment."
      ],
      "faq": [
        {
          "q": "Does the recording get shorter?",
          "a": "No. Each censored passage is replaced by a tone of the same length, so timings elsewhere are unchanged."
        },
        {
          "q": "Can the beep be quieter?",
          "a": "Yes. The level runs from 0 to 1 and the fade softens each end of the tone."
        },
        {
          "q": "Can the original still be recovered from the output?",
          "a": "No. The censored passage is replaced before encoding, so the output file contains the tone and not the original audio."
        }
      ],
      "related": [
        "audio-silence",
        "audio-trim",
        "audio-split"
      ]
    },
    {
      "slug": "audio-channels",
      "name": "Channel tools",
      "category": "audio",
      "one_liner": "Convert mono/stereo, swap, isolate or pan channels via an explicit matrix.",
      "replaces": "Mono/stereo toggles that silently drop a channel",
      "primitive": "Explicit pan-matrix ops for downmix, upmix, swap, isolate and pan",
      "status": "done",
      "runner": "audiotool",
      "script": null,
      "tests": "1/1",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "audio-compress",
      "name": "Compress audio",
      "category": "audio",
      "one_liner": "Shrink audio for podcasts and uploads with visible voice/music/maximum tradeoffs.",
      "replaces": "Compressors that give you one opaque 'compress' button",
      "primitive": "Named intent presets (voice / music / maximum) that set sample rate, channels and bitrate explicitly",
      "status": "done",
      "runner": "audiosound",
      "script": null,
      "tests": "4/6",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Shrink audio files and hear what the smaller file lost",
      "search_description": "Re-encode for podcasts and uploads with visible size reduction and the original on the same transport for comparison. Nothing is uploaded.",
      "search_intro": "Choosing a bitrate is a trade, and every tool that offers the trade makes it invisible: a smaller file arrives and the question of what it cost goes unanswered. This page reports the new size as a percentage of the original and keeps the untouched recording on the same transport as the re-encoded one, so the bitrate is chosen by listening to both rather than by reputation.",
      "how_it_works": [
        "Choose an audio file. The waveform is drawn locally.",
        "Pick a target profile for voice, music or maximum reduction, or set the bitrate directly.",
        "Run, then read the size change and compare the two versions."
      ],
      "use_cases": [
        "Getting a podcast episode under an upload limit without guessing at the bitrate.",
        "Reducing a set of recordings for archiving where some loss is acceptable.",
        "Hearing where a particular recording starts to fall apart as the bitrate drops."
      ],
      "limits": [
        "Lossy re-encoding is not reversible, and re-encoding an already lossy file loses more.",
        "Speech tolerates far lower bitrates than music; one profile does not suit both.",
        "A file that is already small may grow, since the encoder targets a bitrate rather than a size."
      ],
      "faq": [
        {
          "q": "Is the file uploaded?",
          "a": "No. Everything runs in this tab."
        },
        {
          "q": "Why did my file get bigger?",
          "a": "The chosen bitrate is higher than the source's. Lower it, or keep the original."
        },
        {
          "q": "How low can I go?",
          "a": "Compare at each step. Spoken word usually survives much lower settings than music, and the point where it stops working depends on the recording."
        }
      ],
      "related": [
        "audio-convert",
        "audio-normalize",
        "audio-voice-enhance"
      ]
    },
    {
      "slug": "audio-convert",
      "name": "Audio converter",
      "category": "audio",
      "one_liner": "Convert between MP3, WAV, FLAC, AAC, M4A, OGG, Opus, AIFF and WMA with quality-aware, loss-avoiding defaults.",
      "replaces": "Online audio converters that upload your file, cap size, and re-encode lossy to lossy blindly",
      "primitive": "Local ffmpeg with a lossy->lossy WAV intermediate so re-encoding never stacks generational loss",
      "status": "done",
      "runner": "audiotool",
      "script": null,
      "tests": "1/1",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "audio-declick",
      "name": "Remove clicks and pops",
      "category": "audio",
      "one_liner": "Clean clicks and pops off vinyl transfers and damaged recordings.",
      "replaces": "Restoration suites priced for mastering studios",
      "primitive": "Three explicit methods (median, lowpass, FFT) with a strength dial instead of one magic button",
      "status": "done",
      "runner": "audiosound",
      "script": null,
      "tests": "4/6",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Remove clicks and crackle from a vinyl transfer",
      "search_description": "Take impulse noise off a digitised record or a damaged recording and compare the result against the original before downloading. Nothing is uploaded.",
      "search_intro": "Clicks are short and loud, which makes them easy to detect and easy to over-treat: a detector set too eagerly starts removing the attack of every drum and consonant, and the recording goes soft in a way that is hard to notice one pass at a time. This page keeps the untreated source on the same transport as the result, so an over-aggressive setting is audible immediately rather than after the file has been filed away.",
      "how_it_works": [
        "Choose a digitised recording. The waveform is drawn locally.",
        "Pick the method: impulse detection for clicks and pops, smoothing for continuous crackle, or spectral for a mixture.",
        "Set the strength, run, and compare the result against the source."
      ],
      "use_cases": [
        "Cleaning a vinyl or shellac transfer before archiving it.",
        "Removing digital clicks left by a dropout or a bad edit.",
        "Reducing surface crackle without flattening the music underneath."
      ],
      "limits": [
        "A strong setting removes transients that belong to the music along with the clicks, which is the trade the comparison exposes.",
        "Continuous crackle is reduced rather than eliminated, since it overlaps the recording across the whole spectrum.",
        "Damage longer than an impulse, such as a skipped groove, is not repaired here."
      ],
      "faq": [
        {
          "q": "Is the file uploaded?",
          "a": "No. Everything runs in this tab."
        },
        {
          "q": "Which method should I use?",
          "a": "Impulse detection for discrete clicks and pops, smoothing when the noise is continuous crackle, spectral when both are present."
        },
        {
          "q": "Why does it sound duller?",
          "a": "The strength is removing transients that belong to the recording. Lower it and compare again."
        }
      ],
      "related": [
        "audio-denoise",
        "audio-dehum",
        "audio-eq"
      ]
    },
    {
      "slug": "audio-dehum",
      "name": "Remove electrical hum",
      "category": "audio",
      "one_liner": "Notch out 50/60 Hz electrical hum and its harmonics without dulling the rest.",
      "replaces": "One-click denoise that smears the whole spectrum to kill a 60 Hz tone",
      "primitive": "Cascaded notch filters at the mains frequency and its harmonics, nothing else touched",
      "status": "done",
      "runner": "audiosound",
      "script": null,
      "tests": "4/6",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Remove mains hum from a recording",
      "search_description": "Notch out 50 or 60 Hz electrical hum and its harmonics, and hear the result against the original before downloading. Nothing is uploaded.",
      "search_intro": "Mains hum sits at one frequency and its multiples, which makes it the one noise problem with an exact answer: notch the fundamental and the harmonics and leave everything else alone. The difficulty is that a notch deep enough to kill the hum can also hollow out the low end of a voice, and a downloaded file is a slow way to discover that. This page keeps the source and the result on one transport so the trade is audible in the same screen where the frequency and the harmonic count are chosen.",
      "how_it_works": [
        "Choose an audio file. The waveform is drawn locally.",
        "Pick the mains frequency for the region the recording was made in, 50 Hz or 60 Hz.",
        "Choose how many harmonics to notch and how deep the notches go.",
        "Run, then switch the player between source and result to hear what the notches took with them."
      ],
      "use_cases": [
        "Cleaning a buzz picked up from unbalanced cabling or a ground loop.",
        "Rescuing an interview recorded next to a fluorescent light or a dimmer.",
        "Removing hum from a digitised tape without touching the rest of the spectrum."
      ],
      "limits": [
        "Only hum at the mains fundamental and its harmonics is addressed. Broadband hiss needs the noise reducer instead.",
        "Deep notches on the fundamental remove real low-frequency content along with the hum, which is why the comparison is on the page.",
        "Hum that drifts in frequency is only partly caught by fixed notches."
      ],
      "faq": [
        {
          "q": "Is the file uploaded?",
          "a": "No. Everything runs in this tab."
        },
        {
          "q": "Which frequency should I choose?",
          "a": "It follows the electricity supply where the recording was made, not where it is being edited."
        },
        {
          "q": "Why is there still a low rumble?",
          "a": "Rumble is not hum. It is broadband low-frequency energy and is not at a single frequency, so notches do not remove it."
        }
      ],
      "related": [
        "audio-denoise",
        "audio-eq",
        "audio-voice-enhance"
      ]
    },
    {
      "slug": "audio-denoise",
      "name": "Reduce noise",
      "category": "audio",
      "one_liner": "Reduce steady background noise using a sampled noise profile.",
      "replaces": "One-click 'remove noise' that guesses the noise profile for you",
      "primitive": "Sample a real noise clip from the file, then spectral-gate against it (ffmpeg afftdn fallback when the DSP lib is absent)",
      "status": "done",
      "runner": "audiosound",
      "script": null,
      "tests": "9/11",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Reduce background noise with the noise floor measured",
      "search_description": "Sample a passage that is only noise, see the level it measures, and hear the result against the original before downloading. Nothing is uploaded.",
      "search_intro": "Noise reduction is a measurement problem wearing the costume of a button. A reducer that assumes a fixed noise floor either scours a clean recording or barely touches a noisy one, and because the only feedback is a downloaded file, nobody finds out which happened. This tool measures the level of a passage that contains nothing but the noise, hands that measurement to the filter as its noise floor, and reports the number on screen. The amount of reduction stays a separate control, because how loud the noise is and how much of it to remove are different questions.",
      "how_it_works": [
        "Choose an audio file. The waveform is drawn locally and nothing is uploaded.",
        "Point the sample window at a passage that is only room tone, hiss or hum, using the start and length fields.",
        "Set how many decibels of reduction to apply, then run.",
        "Read the measured noise floor on the result, and switch the player between source and result to hear the trade."
      ],
      "use_cases": [
        "Cleaning air-conditioner or computer-fan hiss off an interview recorded in a room.",
        "Reducing tape or preamp hiss on a digitised cassette or minidisc transfer.",
        "Checking how much reduction a recording tolerates before the voice starts to sound processed."
      ],
      "limits": [
        "The sample window must contain only noise. Pointed at speech, it measures the speech and treats it as the floor.",
        "Spectral reduction removes noise that overlaps the voice by attenuating shared frequencies, so a heavy setting audibly thins the recording. The comparison exists so that is a decision rather than a surprise.",
        "This is a filter, not a speech model; it does not separate a voice from a competing voice or from music.",
        "A source with no quiet passage anywhere cannot be measured honestly, and the tool says when the window could not be read instead of assuming a value."
      ],
      "faq": [
        {
          "q": "Is the file uploaded?",
          "a": "No. The measurement, the filtering, the waveforms and the playback all happen in this tab. The audio engine is fetched once and cached."
        },
        {
          "q": "What does the measured noise floor mean?",
          "a": "It is the average level of the sample window in decibels, and it is handed to the filter as the level below which audio is treated as noise."
        },
        {
          "q": "How much reduction should I use?",
          "a": "Start at the default and raise it until the result begins to sound hollow, comparing against the source each time. There is no correct number for every recording."
        }
      ],
      "related": [
        "audio-dehum",
        "audio-declick",
        "audio-voice-enhance"
      ]
    },
    {
      "slug": "audio-duck",
      "name": "Voice over music",
      "category": "audio",
      "one_liner": "Narration over music with auto-ducking, the podcast mix without the DAW.",
      "replaces": "Manually riding the music fader under every line of narration",
      "primitive": "Sidechain auto-ducking: music at full volume until voice is present, then it gets out of the way",
      "status": "done",
      "runner": "audiocompose",
      "script": null,
      "tests": "6/6",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Duck music under narration by a number of decibels",
      "search_description": "Put a voice over a music bed so the music drops while the voice speaks and returns in the gaps, by a depth you set in decibels. Local, no upload.",
      "search_intro": "Ducking means the music moves: full level in the gaps, down under the voice, back up afterwards. A control that simply makes the music quiet for the whole piece is not ducking, and it is a common substitute because it is easier to implement and hard to tell apart from a description. This page keeps the two ideas separate. The resting level of the music is one control, and how far the music drops under the voice is another, given in decibels. The depth is reached by measuring how loud the voice actually is and solving the compressor's threshold against that reading, because a fixed threshold produces a different depth on every recording.",
      "how_it_works": [
        "Choose the voice first and the music second. Both appear on one timeline at one shared scale.",
        "Set how far the music should drop while the voice speaks, and its resting level for the gaps.",
        "Mix, then read the panel showing the measured voice peak and the threshold that was solved from it."
      ],
      "use_cases": [
        "Mix a podcast intro where the bed drops under the host and comes back between sentences.",
        "Put a music bed under a recorded voiceover for a video without editing by hand.",
        "Produce an audio ad where the bed has to stay audible but never compete with the read."
      ],
      "limits": [
        "Exactly two files are used, voice then music. A third is refused rather than silently ignored.",
        "The depth is solved from the peak level of the voice as a whole. A recording whose level varies widely will duck less in its quiet passages than in its loud ones, which is usually what is wanted but is not a fixed number.",
        "When the voice cannot be measured the page falls back to a documented default and says on the result panel that it did."
      ],
      "faq": [
        {
          "q": "Are the files uploaded?",
          "a": "No. The measurement pass, the mix and the read-back all happen in this tab. The audio engine is fetched once and cached."
        },
        {
          "q": "Why is the music no longer quiet throughout?",
          "a": "Because that was not ducking. The music now sits at its own level and drops only while the voice is present, by the depth set on the page."
        },
        {
          "q": "What is the threshold figure on the result panel?",
          "a": "It is the level the compressor treats as the point where the voice starts pushing the music down, solved from the measured peak of the voice so that the requested depth is reached."
        }
      ],
      "related": [
        "audio-overlay",
        "audio-voice-enhance",
        "audio-normalize"
      ]
    },
    {
      "slug": "audio-dynamics",
      "name": "Dynamic range compressor",
      "category": "audio",
      "one_liner": "Even out loud and quiet parts with presets or full manual control.",
      "replaces": "Compressor plugins that assume you own a DAW to host them",
      "primitive": "FFmpeg acompressor with named presets (vocal, podcast, limiter) and full manual parameter override",
      "status": "done",
      "runner": "audiosound",
      "script": null,
      "tests": "4/6",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Even out loud and quiet parts of a recording",
      "search_description": "Compress the dynamic range with presets or full manual control, and compare against the original on one transport. Nothing is uploaded.",
      "search_intro": "Compression is judged by what it costs, not by what it does. Pulled too far it removes the breathing room that made a performance sound alive, and since it also makes everything louder, the processed version wins a blind comparison for reasons that have nothing to do with quality. This page keeps both versions on one transport and draws both waveforms on a shared scale, so the flattening is visible as a shape even when it is flattering to the ear.",
      "how_it_works": [
        "Choose an audio file. The waveform is drawn locally.",
        "Pick a preset, or set threshold, ratio, attack and release directly.",
        "Run, then compare the result against the source and look at how much the waveform shape changed."
      ],
      "use_cases": [
        "Evening out an interview where one speaker is much quieter than the other.",
        "Controlling peaks before setting a loudness target.",
        "Learning what a ratio actually does by hearing and seeing it against the original."
      ],
      "limits": [
        "Compression cannot separate a quiet voice from loud background noise; it raises both.",
        "Heavy settings pump audibly on material with strong rhythm.",
        "The result is louder, which biases a listening comparison in its favour; the waveform shape is the check on that."
      ],
      "faq": [
        {
          "q": "Is the file uploaded?",
          "a": "No. Everything runs in this tab."
        },
        {
          "q": "Should I compress or normalize?",
          "a": "Compression changes the distance between loud and quiet parts. Normalizing moves the whole recording to a target level and changes nothing else."
        },
        {
          "q": "What order should these go in?",
          "a": "Clean the recording first, then compress, then set the loudness target last."
        }
      ],
      "related": [
        "audio-normalize",
        "audio-volume",
        "audio-eq"
      ]
    },
    {
      "slug": "audio-eq",
      "name": "Parametric EQ",
      "category": "audio",
      "one_liner": "Shape the tone with EQ presets or your own multi-band curve.",
      "replaces": "EQ locked inside player apps that will not export the result",
      "primitive": "FFmpeg equalizer chains with tonal presets (bass boost, vocal clarity) and manual multi-band config",
      "status": "done",
      "runner": "audiosound",
      "script": null,
      "tests": "4/6",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Equalize audio with presets or your own bands",
      "search_description": "Shape the tone with a preset or a typed multi-band curve and hear it against the untouched file. Nothing is uploaded.",
      "search_intro": "Equalization is the one audio edit where a small change can be inaudible and a large one irreversible, and where the ear adapts within seconds of hearing only the result. The defence is an immediate comparison: this page keeps the original and the equalized version on one transport, and draws both waveforms on a single shared scale so a change in level is visible as well as audible.",
      "how_it_works": [
        "Choose an audio file. The waveform is drawn locally.",
        "Pick a preset, or type a band list to override it with exact frequencies, widths and gains.",
        "Run, then switch between source and result."
      ],
      "use_cases": [
        "Lifting the presence range so a voice cuts through on small speakers.",
        "Rolling off rumble below the useful content of a recording.",
        "Applying an exact, repeatable band list rather than dragging an unlabelled curve."
      ],
      "limits": [
        "Boosting a frequency that carries no content amplifies the noise there instead.",
        "The band list is parsed strictly; a band missing a frequency, width or gain is refused rather than guessed at.",
        "Output is re-encoded, so a lossy source stays lossy."
      ],
      "faq": [
        {
          "q": "Is the file uploaded?",
          "a": "No. Everything runs in this tab."
        },
        {
          "q": "What does the band syntax mean?",
          "a": "Each band names a centre frequency, a filter type, a width and a gain in decibels, and bands are separated by a vertical bar."
        },
        {
          "q": "Why did the level change?",
          "a": "Boosting bands raises overall level. The two waveforms share one scale so the change is visible, and the loudness tool sets a target afterwards."
        }
      ],
      "related": [
        "audio-dynamics",
        "audio-normalize",
        "audio-voice-enhance"
      ]
    },
    {
      "slug": "audio-extract-from-video",
      "name": "Extract audio from video",
      "category": "audio",
      "one_liner": "Pull the audio track out of MP4, MKV, MOV, WebM and more into any audio format.",
      "replaces": "'Extract audio' web tools that upload the whole video just to strip the track",
      "primitive": "Decode only the audio stream locally; the video never leaves the machine",
      "status": "done",
      "runner": "audiotool",
      "script": null,
      "tests": "1/1",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "audio-karaoke",
      "name": "Basic vocal remover",
      "category": "audio",
      "one_liner": "Remove center-panned vocals via phase cancellation (basic, honest about its limits).",
      "replaces": "'Remove vocals' tools that promise AI stems and deliver mush",
      "primitive": "Honest center-channel phase cancellation, labeled basic rather than sold as stem separation",
      "status": "done",
      "runner": "audiosound",
      "script": null,
      "tests": "4/6",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Remove center-panned vocals by phase cancellation",
      "search_description": "Cancel what is common to both channels and hear exactly what that costs, against the original. Honest about what it cannot do. Nothing is uploaded.",
      "search_intro": "Phase cancellation removes whatever sits equally in both channels. On most mixes that is the lead vocal, and also the kick drum, the bass and anything else placed in the middle. It is not vocal separation and no amount of tuning turns it into vocal separation. The useful thing a page can do is make the cost audible in one click, which is what the comparison here is for.",
      "how_it_works": [
        "Choose a stereo file. A mono file has nothing to cancel against and is not a candidate.",
        "Pick the method and run.",
        "Compare the result against the source to hear what else disappeared with the vocal."
      ],
      "use_cases": [
        "Making a rough backing track from a stereo mix.",
        "Checking quickly whether a particular mix responds to cancellation at all.",
        "Hearing what is centre-panned in a mix."
      ],
      "limits": [
        "Bass, kick and any other centred instrument is removed along with the vocal.",
        "A vocal with stereo reverb or double-tracking leaves audible residue.",
        "A mono source cannot be processed this way at all.",
        "This is phase cancellation, not source separation, and it does not use a model."
      ],
      "faq": [
        {
          "q": "Is the file uploaded?",
          "a": "No. Everything runs in this tab."
        },
        {
          "q": "Why does the bass disappear too?",
          "a": "Bass is usually centred, and cancellation removes what is common to both channels regardless of what it is."
        },
        {
          "q": "Can this isolate the vocal instead?",
          "a": "No. This method removes what is centred; it cannot produce it on its own."
        }
      ],
      "related": [
        "audio-channels",
        "audio-eq",
        "audio-dynamics"
      ]
    },
    {
      "slug": "audio-loop",
      "name": "Loop audio",
      "category": "audio",
      "one_liner": "Loop a file N times or out to a target duration, seamless if you want.",
      "replaces": "Copy-pasting a track four times on a timeline to fill two minutes",
      "primitive": "Loop N times or to a target duration, with optional crossfade at the seams",
      "status": "done",
      "runner": "audiocompose",
      "script": null,
      "tests": "8/8",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Loop audio to a length that is actually the length you asked for",
      "search_description": "Repeat a clip a set number of times or out to a target duration, cut to that duration exactly, with an optional crossfade at each join. Local, no upload.",
      "search_intro": "A target duration is a promise about the finished file, and a looper that repeats until it has passed the target and then stops has not kept it. Asking for sixty seconds from a seven second clip gives sixty three, and adding a crossfade makes the error change sign, because each overlap removes time the repeat count never accounted for. This page counts the repeats with the overlap included and then cuts the result to the target, so the number on the page and the length of the file are the same number. Whole repetitions are still available as an explicit choice rather than as the only behaviour.",
      "how_it_works": [
        "Choose one file. It is decoded in this tab and drawn against the timeline the current settings imply.",
        "Loop by a repeat count, or by a target duration and let the page work out the repeats.",
        "Render, then compare the predicted length against the length read back from the produced file."
      ],
      "use_cases": [
        "Extend a short music bed to the exact runtime of a video.",
        "Turn a loop into an hour of background audio for a stream or a workspace.",
        "Repeat a test tone or a metronome bar out to a fixed practice length."
      ],
      "limits": [
        "Cutting to an exact target re-encodes, because a stream copy can only cut on a frame boundary. The page says which of the two happened and why.",
        "A stream copy of a lossy source carries an encoder padding frame into every join, so the seams are not sample accurate. The page reports the padding per join rather than claiming a seamless result.",
        "Whether a loop is audible at the join depends on how the source starts and ends. No looper can make an arbitrary clip meet itself cleanly."
      ],
      "faq": [
        {
          "q": "Is the file uploaded?",
          "a": "No. Decoding, looping and the read-back all happen in this tab. The audio engine is fetched once and cached."
        },
        {
          "q": "Why did a sixty second target used to give sixty three seconds?",
          "a": "The repeat count was rounded up and the result was never cut. It is now cut to the target, and the whole-repetition behaviour is a checkbox."
        },
        {
          "q": "Does a crossfade change how many repeats are needed?",
          "a": "Yes. Every join gives up the crossfade length, so more repeats are needed to reach the same target. That is now part of the count."
        }
      ],
      "related": [
        "audio-merge",
        "audio-trim",
        "audio-ringtone"
      ]
    },
    {
      "slug": "audio-merge",
      "name": "Merge audio",
      "category": "audio",
      "one_liner": "Join multiple audio files into one, with an optional crossfade.",
      "replaces": "Joiners that glue mismatched files and produce clicks at the seams",
      "primitive": "Normalize every input to a common format first, then concat (optionally crossfade)",
      "status": "done",
      "runner": "audiocompose",
      "script": null,
      "tests": "5/5",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Join audio files in an order you can see and change",
      "search_description": "Put several recordings end to end, with the order on a timeline and an optional crossfade at each join. Runs locally, nothing is uploaded.",
      "search_intro": "Joining files is an ordering job, and most joiners treat the order as whatever the file picker handed over. This page puts every file on one timeline at one shared amplitude scale, so a track that is quieter than the rest is visible before anything is rendered, and the order is moved with buttons rather than only by dragging. A crossfade overlaps each join, which makes the result shorter than the sum of the parts by one overlap per join; the page states the length the settings predict and then, after the render, the length the finished file actually has.",
      "how_it_works": [
        "Choose two or more files. Each one is decoded in this tab and drawn as a track on the shared timeline.",
        "Move tracks up and down until the order is the one you want, and set a crossfade if the joins should overlap.",
        "Join them, then compare the predicted length against the length read back from the produced file."
      ],
      "use_cases": [
        "Assemble interview segments or takes into one continuous recording.",
        "Build a continuous set from separate tracks with a crossfade at each change.",
        "Concatenate chapters into a single long file for a player that has no queue."
      ],
      "limits": [
        "A crossfade at or past the whole length of the shortest file is refused by name, because the overlap would consume that file entirely and the result would be shorter than the inputs.",
        "Every input is converted to a common sample rate and channel count before joining, taking the highest of each, so a mono file in a stereo set is upmixed.",
        "Trimming inside a track belongs to the trim tool; this page joins whole files."
      ],
      "faq": [
        {
          "q": "Are the files uploaded?",
          "a": "No. Decoding, drawing and joining all happen in this tab. The audio engine is fetched once and cached."
        },
        {
          "q": "Why is the result shorter than the sum of the files?",
          "a": "A crossfade overlaps the end of one file with the start of the next, so each join gives up that much time. With no crossfade the result is the exact sum."
        },
        {
          "q": "Can the order be changed after adding files?",
          "a": "Yes. Each track has up and down controls, and the timeline redraws in the new order."
        }
      ],
      "related": [
        "audio-overlay",
        "audio-loop",
        "audio-trim"
      ]
    },
    {
      "slug": "audio-metadata",
      "name": "Edit metadata",
      "category": "audio",
      "one_liner": "Set title, artist, album, year, genre and track tags.",
      "replaces": "Tag editors locked to a single container format",
      "primitive": "One tag interface over ID3, Vorbis comments and MP4 atoms",
      "status": "done",
      "runner": "audiotags",
      "script": null,
      "tests": "7/9",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Edit audio tags after seeing the ones already there",
      "search_description": "Read the title, artist, album and every other tag a file already carries, change them, remove them, and read the written file back to check. Local, no upload.",
      "search_intro": "Most tag editors present empty boxes and ask what the tags should be, which means retyping values the file already holds and guessing at whatever is not shown. This one opens by reading the file: each field arrives filled in with what the container actually says, every embedded picture is listed at its real format and pixel size, and the facts a container reports about itself are shown without being offered for editing. The written file is then reopened and read a second time, so what appears on the page after a write comes from the produced file rather than from the request that made it.",
      "how_it_works": [
        "Choose one audio file or a batch. Each is read in this tab and its current tags appear in the fields.",
        "Edit any field, tick remove beside a tag to drop it, or clear every tag at once. Artwork is a stream rather than a tag and is kept either way.",
        "Write the file, then read the result panel, which is filled from a second read of the file that was just produced."
      ],
      "use_cases": [
        "Correct the artist or album on files ripped or downloaded with the wrong tags.",
        "Strip personal comments, encoder notes and stray tags before sharing a recording.",
        "Check what a file actually carries before sending it to a distributor or a player that reads tags strictly."
      ],
      "limits": [
        "Tags are written through the container's generic key and value interface, so lyrics, chapters, ratings and replay gain are out of reach.",
        "MP3 output is written as ID3 version 2.3, which more players and file browsers read than version 2.4. ID3 version 1 is not written alongside it.",
        "The picture role byte that distinguishes a front cover from a back cover is not reachable, so a written cover is simply a cover."
      ],
      "faq": [
        {
          "q": "Is the file uploaded?",
          "a": "No. Reading the tags, writing the new file and reading it back all happen in this tab. The audio engine is fetched once and cached."
        },
        {
          "q": "Does editing a tag re-encode the audio?",
          "a": "No. Every stream is copied, so the audio and any embedded artwork come through untouched and only the tags are rewritten."
        },
        {
          "q": "Why does an empty field not delete the tag?",
          "a": "An empty field cannot say whether it was cleared or never filled. Removal is a separate instruction, which is what the remove box beside each field does."
        }
      ],
      "related": [
        "audio-album-art",
        "audio-convert",
        "audio-analyze"
      ]
    },
    {
      "slug": "audio-normalize",
      "name": "Normalize loudness",
      "category": "audio",
      "one_liner": "Normalize loudness to a target LUFS (or peak/RMS) via a measured two-pass pass.",
      "replaces": "'Normalize' buttons that only peak-normalize",
      "primitive": "Broadcast-standard two-pass EBU R128 loudnorm (LUFS), with peak and RMS as explicit alternatives",
      "status": "done",
      "runner": "audiosound",
      "script": null,
      "tests": "4/6",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Normalize loudness to a LUFS target with the measurement shown",
      "search_description": "Measure the integrated loudness, true peak and range of a recording, then move it to a target. The measured numbers are reported, not just the target. Nothing is uploaded.",
      "search_intro": "Every service that plays audio back turns it down to its own loudness target, so a file mastered louder than that target is not louder on arrival, only more compressed on the way. Getting this right means knowing two numbers: where the recording sits and where it should sit. Most browser normalizers show neither and return a file. This one runs a real loudness analysis, reports the integrated loudness, true peak and loudness range it measured, and then applies the correction to the target chosen here.",
      "how_it_works": [
        "Choose an audio file. The waveform is drawn locally.",
        "Pick the method: integrated loudness for anything destined for a streaming service, or peak and average level for simpler cases.",
        "Set the target and run.",
        "Read the measured loudness of the input beside the target, and compare the result against the source."
      ],
      "use_cases": [
        "Bringing a podcast episode to a consistent level across a series.",
        "Finding out where a recording actually sits before deciding whether it needs anything at all.",
        "Matching a set of files to one target so a playlist does not jump between tracks."
      ],
      "limits": [
        "Integrated loudness is a measurement over a whole recording; a short clip or a clip that is mostly silence produces a figure that is technically correct and practically misleading.",
        "Where the correction would push the true peak past its ceiling, the loudness filter falls back to a dynamic mode, so the reported measurement describes the input rather than guaranteeing the output.",
        "Loudness normalization does not repair clipping that is already in the recording."
      ],
      "faq": [
        {
          "q": "Is the file uploaded?",
          "a": "No. The analysis and the correction both run in this tab."
        },
        {
          "q": "What target should I use?",
          "a": "Streaming services publish their own; the default here is the common one for speech and music delivery. The right answer depends on where the file is going."
        },
        {
          "q": "Why is the measured number different from the target after processing?",
          "a": "The reported measurement is of the input. It is shown so the size of the correction is visible rather than hidden."
        }
      ],
      "related": [
        "audio-volume",
        "audio-dynamics",
        "audio-compress"
      ]
    },
    {
      "slug": "audio-overlay",
      "name": "Overlay two tracks",
      "category": "audio",
      "one_liner": "Mix one track over another with volume, delay and fade control.",
      "replaces": "Opening an editor just to lay one sound over another",
      "primitive": "Two files in, one mix out; per-track volume, overlay delay and fade-in as flags",
      "status": "done",
      "runner": "audiocompose",
      "script": null,
      "tests": "7/7",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Lay one track over another without the base dropping",
      "search_description": "Mix an overlay onto a base track with independent levels, a start offset and a fade, and keep the base at the level you set for the whole file. Local, no upload.",
      "search_intro": "Mixing two tracks is a level job, and the usual mixing filter quietly rescales its inputs by how many of them are currently playing. The result is a base that sits six decibels down for exactly as long as the overlay runs and jumps back the instant it ends, which is heard as a step in the middle of the mix rather than as a setting anyone chose. This page turns that rescaling off, so each level is the level asked for, and then says what the two peaks sum to, because summing without a rescale is what makes clipping possible. A limiter is offered as a decision rather than applied as a silent correction.",
      "how_it_works": [
        "Choose the base first and the overlay second. Both appear on one timeline, the overlay drawn at the offset you set.",
        "Set each level, the start offset and any fade-in, and read the worst-case summed peak the page reports.",
        "Mix, then check the receipt read back from the produced file."
      ],
      "use_cases": [
        "Put a music bed under narration that is already recorded.",
        "Drop a sound effect at a known second inside a longer recording.",
        "Layer an ambience track under dialogue without either being rescaled."
      ],
      "limits": [
        "Exactly two files are used. A third is refused rather than silently ignored, which is what happened before.",
        "The two levels are summed as set, so a loud pair can pass full scale. The page reports the worst case and offers a limiter; it does not quietly turn anything down.",
        "For narration that should push the music down only while it speaks, the ducking tool is the right one; this page holds both levels constant."
      ],
      "faq": [
        {
          "q": "Are the files uploaded?",
          "a": "No. Decoding, mixing and the read-back all happen in this tab. The audio engine is fetched once and cached."
        },
        {
          "q": "Why did my base used to get quieter and then louder again?",
          "a": "The mixing filter normalizes by the number of active inputs by default, so the base was halved while the overlay played. That normalization is now off."
        },
        {
          "q": "What does the summed peak figure mean?",
          "a": "It is the worst case, the level reached if both peaks landed on the same sample. Real material usually peaks lower."
        }
      ],
      "related": [
        "audio-duck",
        "audio-merge",
        "audio-volume"
      ]
    },
    {
      "slug": "audio-pitch",
      "name": "Change pitch",
      "category": "audio",
      "one_liner": "Shift pitch by semitones. Tempo shifts with pitch.",
      "replaces": "Pitch shifters that drag the tempo along with the pitch",
      "primitive": "Semitone-based shift with an optional tempo-preserving mode and an asetrate fallback",
      "status": "done",
      "runner": "audiotool",
      "script": null,
      "tests": "1/1",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "audio-rearrange",
      "name": "Rearrange clips",
      "category": "audio",
      "one_liner": "Cut listed segments out of a file and rejoin them in any order.",
      "replaces": "Razor-blade editing for a simple reorder of known segments",
      "primitive": "A range list like 0-15,30-45,15-30 extracts and concatenates in that order, crossfade optional",
      "status": "done",
      "runner": "audioregions",
      "script": null,
      "tests": "7/7",
      "from": null,
      "to": null,
      "public": false,
      "search_title": "Reorder sections of an audio file",
      "search_description": "Pick clips on the waveform, put them in the order you want, and export one joined file. Nothing is uploaded.",
      "search_intro": "Reordering is the one audio job where the list order is the output, and a comma separated string of ranges hides that completely. This workspace makes each clip a row that can be moved earlier or later, with the waveform showing where every clip comes from. The clips are extracted through a lossless intermediate and joined in list order, optionally with a crossfade, so the exported file follows the arrangement on screen.",
      "how_it_works": [
        "Choose an audio file and review the waveform.",
        "Add one clip per section and set its bounds by dragging, by keyboard, or by typing.",
        "Move clips earlier or later until the row order matches the arrangement wanted.",
        "Set a crossfade if the joins should be smoothed.",
        "Run to build the joined file locally and download it."
      ],
      "limits": [
        "Clip order is the output order; the list is never sorted by time, which is what separates this from every other region tool here.",
        "A crossfade longer than the shortest clip is reduced to half that clip's length.",
        "The same passage can appear more than once, and each appearance is extracted separately."
      ],
      "use_cases": [
        "Moving an answer before its question in an interview edit.",
        "Assembling a highlight reel from one long recording.",
        "Building a rough arrangement from takes inside a single file."
      ],
      "faq": [
        {
          "q": "Can a clip be used twice?",
          "a": "Yes. Add it as two rows; each is extracted and placed independently."
        },
        {
          "q": "Does the order follow the waveform?",
          "a": "No. The row order is the output order, so a later passage can be placed first."
        },
        {
          "q": "What does the crossfade do at the joins?",
          "a": "It overlaps each pair of neighbouring clips for the chosen number of seconds instead of butting them together."
        }
      ],
      "related": [
        "audio-split",
        "audio-trim",
        "audio-merge"
      ]
    },
    {
      "slug": "audio-reverse",
      "name": "Reverse audio",
      "category": "audio",
      "one_liner": "Play it backwards. Format preserved unless you say otherwise.",
      "replaces": "Web toys that upload your file to play it backwards",
      "primitive": "FFmpeg areverse, format preserved by default, re-encode only if you ask",
      "status": "done",
      "runner": "audiotool",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "audio-ringtone",
      "name": "Make a ringtone",
      "category": "audio",
      "one_liner": "Cut a 30s iPhone (M4R) or Android (MP3) ringtone with fades.",
      "replaces": "Ringtone makers that upsell the export or the length",
      "primitive": "Length-clamped trim+fade encoded straight to M4R (iPhone) or MP3 (Android)",
      "status": "done",
      "runner": "audioregions",
      "script": null,
      "tests": "7/7",
      "from": null,
      "to": null,
      "public": false,
      "search_title": "Make a ringtone from any audio file",
      "search_description": "Choose the hook on a waveform and export it as an iPhone .m4r or an Android .mp3, inside the length the phone accepts. Nothing is uploaded.",
      "search_intro": "The part of a song that works as a ringtone is a few seconds long and almost never where a typed start time lands. This workspace draws the waveform, lets the selection be dragged or nudged by keyboard, and plays it back before encoding. It also enforces the one constraint that silently breaks ringtones: iOS refuses to sync an .m4r longer than 40 seconds, so a longer selection is refused here with the reason instead of producing a file the phone ignores.",
      "how_it_works": [
        "Choose an audio file and review the waveform.",
        "Drag the selection over the passage wanted, or nudge either edge with the arrow keys.",
        "Play the selection to check where it starts and ends.",
        "Pick iPhone or Android, add fades if the cut is abrupt, and run.",
        "Download the .m4r or .mp3 and move it to the phone."
      ],
      "use_cases": [
        "Cutting a ringtone out of a track already owned.",
        "Making a short alert tone from a recording.",
        "Producing the same hook in both phone formats."
      ],
      "limits": [
        "An iPhone ringtone must be 40 seconds or shorter to sync; a longer selection is refused rather than encoded.",
        "The iPhone output is AAC in an ipod container with the .m4r extension; Android output is MP3.",
        "Installing the file on the phone is a manual step this tool does not perform."
      ],
      "faq": [
        {
          "q": "Why is 40 seconds the limit?",
          "a": "It is Apple's constraint. An .m4r longer than 40 seconds will not sync, so it is refused here rather than produced."
        },
        {
          "q": "How do I install it on an iPhone?",
          "a": "Transfer the .m4r to the device through Finder or iTunes; it then appears under ringtones in the sound settings."
        },
        {
          "q": "Does Android have the same limit?",
          "a": "No. The Android MP3 output has no length cap here."
        }
      ],
      "related": [
        "audio-trim",
        "audio-split",
        "audio-convert"
      ]
    },
    {
      "slug": "audio-silence",
      "name": "Remove or insert silence",
      "category": "audio",
      "one_liner": "Remove or insert silence with explicit threshold and duration controls.",
      "replaces": "Silence removers stuck on one fixed threshold",
      "primitive": "Explicit threshold + min-duration for removal, and position + duration for insertion",
      "status": "done",
      "runner": "audioregions",
      "script": null,
      "tests": "7/7",
      "from": null,
      "to": null,
      "public": false,
      "search_title": "Remove silence from a recording, after reviewing it",
      "search_description": "See the detected silences on the waveform, adjust or delete any of them, then rebuild the file from what is left. Nothing is uploaded.",
      "search_intro": "A silence threshold applied without review is the fastest way to lose a quiet word. This workspace detects the silent passages, draws them on the waveform, and lets every one of them be adjusted or removed from the list before anything is cut. Detection runs two ways and says which one produced the current regions: an instant browser estimate, and a measured pass by the audio engine that performs the cut. The export rebuilds the file from the material between the reviewed regions, so what is removed is exactly what was shown.",
      "how_it_works": [
        "Choose an audio file; an estimate of the silent passages appears on the waveform immediately.",
        "Adjust the threshold and minimum length, then re-estimate, or measure with the audio engine for its own answer.",
        "Edit or delete individual regions, and use play with regions skipped to hear the result first.",
        "Run to rebuild the file from the kept material and download it."
      ],
      "use_cases": [
        "Tightening the pauses in a podcast or interview.",
        "Trimming dead air from the front and back of a voice memo.",
        "Cleaning up gaps in a lecture recording before publishing."
      ],
      "limits": [
        "The regions shown on load are a browser-side estimate; the measured pass comes from the audio engine's own silencedetect and is labelled as such.",
        "Silence running to the end of the file has no detected end point, so it is closed at the source length.",
        "Regions covering the entire file are refused, since nothing would remain."
      ],
      "faq": [
        {
          "q": "What is the difference between estimated and measured?",
          "a": "The estimate is computed from the decoded waveform in this tab and is instant. The measured pass runs the audio engine's own detection, which is the same engine that performs the cut."
        },
        {
          "q": "Can I keep a pause the detector flagged?",
          "a": "Yes. Remove that region from the list and it stays in the output."
        },
        {
          "q": "Can silence be added instead?",
          "a": "The local command line tool has an insert mode for placing a gap at a chosen position."
        }
      ],
      "related": [
        "audio-trim",
        "audio-split",
        "audio-censor"
      ]
    },
    {
      "slug": "audio-speed",
      "name": "Change speed",
      "category": "audio",
      "one_liner": "Change playback speed. Pitch shifts with speed.",
      "replaces": "Speed changers that chipmunk the pitch up with the tempo",
      "primitive": "atempo with an optional pitch-preserving mode",
      "status": "done",
      "runner": "audiotool",
      "script": null,
      "tests": "1/1",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "audio-split",
      "name": "Split audio",
      "category": "audio",
      "one_liner": "Split one file into many by duration, segment count, or detected silence.",
      "replaces": "Splitters that only cut by a single fixed duration",
      "primitive": "Three explicit split modes: equal duration, N segments, or silence-detected gaps",
      "status": "done",
      "runner": "audioregions",
      "script": null,
      "tests": "7/7",
      "from": null,
      "to": null,
      "public": false,
      "search_title": "Split audio at cut points you can see",
      "search_description": "Place cut points on the waveform and export the segments as separate files, numbered in order. Nothing is uploaded.",
      "search_intro": "Splitting by a chunk length is a guess about where the music or the speech actually breaks. This workspace puts the cut points on the waveform, so each boundary and the segment it opens can be reviewed and played before export. Every segment is written as its own file, numbered in source order, and the run is refused outright when the length of the source cannot be read, which is the case that previously produced silently empty output.",
      "how_it_works": [
        "Choose an audio file and review the drawn waveform.",
        "Drag the cut point, add more with the Add cut point control, or type exact times.",
        "Play from any cut point to hear where the boundary falls.",
        "Pick an output format and run; each segment is encoded locally.",
        "Download the segments individually or as one ZIP."
      ],
      "use_cases": [
        "Breaking a long recording into per-track or per-topic files.",
        "Splitting a lecture at the points where the subject changes.",
        "Cutting a DJ set into individual pieces at the transitions."
      ],
      "limits": [
        "Segment files are named with the source name plus a three digit index, so ordering survives a file listing.",
        "Cut points closer than 0.05 seconds apart are refused, because they would produce a segment shorter than one encoder frame.",
        "A source whose length the browser cannot read is refused instead of split, since every boundary depends on that length."
      ],
      "faq": [
        {
          "q": "How are the output files named?",
          "a": "The source name, an underscore, and a zero padded index: recording_001.mp3, recording_002.mp3, and so on."
        },
        {
          "q": "Can I still split by chunk length?",
          "a": "The local command line tool keeps the chunk length, equal segments and silence gap modes. The browser workspace uses reviewed cut points so the picture matches the result."
        },
        {
          "q": "Is there a limit on the number of segments?",
          "a": "No fixed limit, but every segment is encoded in this tab, so a very large number takes proportionally longer."
        }
      ],
      "related": [
        "audio-trim",
        "audio-silence",
        "audio-rearrange"
      ]
    },
    {
      "slug": "audio-tone",
      "name": "Tone and noise generator",
      "category": "audio",
      "one_liner": "Generate test tones and noise at a chosen frequency, duration and level.",
      "replaces": "Ad-laden tone-generator websites",
      "primitive": "Local generation of sine/square/saw tones and white/pink/brown noise at exact parameters",
      "status": "done",
      "runner": "audiotool",
      "script": null,
      "tests": "1/1",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "audio-trim",
      "name": "Trim audio",
      "category": "audio",
      "one_liner": "Cut audio to a start/end range with optional fade in and out.",
      "replaces": "Trimmers that re-encode the whole clip to cut two seconds",
      "primitive": "Timestamp-accurate atrim with optional fades, keeping the codec where possible",
      "status": "done",
      "runner": "audioregions",
      "script": null,
      "tests": "7/7",
      "from": null,
      "to": null,
      "public": false,
      "search_title": "Trim audio on a waveform, in the browser",
      "search_description": "Cut a clip to a start and end you can see and hear, with fades and a remove-the-middle mode. Nothing is uploaded.",
      "search_intro": "A timestamp typed into a box is a guess. This workspace draws the waveform, puts the start and end on it as draggable, keyboard-operable handles, and plays the selection back before anything is encoded. The cut it performs is the range it drew: the same region array produces the picture and the engine arguments. A range that cannot produce a real file, such as an end before its start or a start past the end of the recording, is refused with the reason rather than handed to the encoder.",
      "how_it_works": [
        "Choose an audio file; its length is read from the browser's own media decoder and the waveform is drawn locally.",
        "Drag either edge of the selection, or focus an edge and use the arrow keys, Home and End.",
        "Play the selection to hear exactly what will be kept, then set fades if wanted.",
        "Switch on remove-the-middle to keep the outside of the selection instead of the inside.",
        "Run to encode the result locally and download it."
      ],
      "use_cases": [
        "Cutting an intro and outro off a recording.",
        "Pulling a single quote out of an interview.",
        "Removing a section from the middle of a take without opening a full editor."
      ],
      "limits": [
        "Remove-the-middle rebuilds the file from the surviving sides through a lossless intermediate; a selection covering the whole file is refused.",
        "The waveform is drawn only for clips short enough to decode in a browser; longer clips keep numeric and playhead-driven region entry.",
        "The output is re-encoded in the chosen format, so a lossy source stays lossy."
      ],
      "faq": [
        {
          "q": "Is the file uploaded?",
          "a": "No. Decoding, the waveform, playback and the encode all happen in this tab. The audio engine is fetched once and cached."
        },
        {
          "q": "Why was my range refused?",
          "a": "A range whose end is at or before its start, or whose start is past the end of the recording, cannot produce audio. The message names which one applies."
        },
        {
          "q": "Can I keep the outside instead of the inside?",
          "a": "Yes. Remove-the-middle keeps everything outside the selection and joins the two sides."
        }
      ],
      "related": [
        "audio-split",
        "audio-ringtone",
        "audio-silence"
      ]
    },
    {
      "slug": "audio-voice-enhance",
      "name": "Enhance voice",
      "category": "audio",
      "one_liner": "Clean up spoken-word audio with a podcast/broadcast/phone filter chain.",
      "replaces": "'Enhance voice' AI toggles you cannot see or adjust",
      "primitive": "A named EQ+compression chain (podcast/broadcast/phone) you can read and tweak",
      "status": "done",
      "runner": "audiosound",
      "script": null,
      "tests": "4/6",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Clean up spoken-word audio and hear the difference",
      "search_description": "Apply a podcast, broadcast or phone voice chain and compare it against the untouched recording on one transport. Nothing is uploaded.",
      "search_intro": "A voice chain is several decisions at once: what to cut below the voice, what to lift in the presence range, and how hard to even out the level. Presets make those decisions quickly and hide them completely, so the only way to judge one is to hear it against the recording it replaced. This page puts both on the same transport and reports the size of what came out, so choosing a chain is a comparison rather than a guess.",
      "how_it_works": [
        "Choose a spoken-word recording. The waveform is drawn locally.",
        "Pick a chain: podcast, broadcast, or phone.",
        "Run, then switch between source and result to hear what the chain did."
      ],
      "use_cases": [
        "Preparing an interview for a podcast without opening a full editor.",
        "Making a lecture recording intelligible on laptop speakers.",
        "Deciding which chain suits a particular voice by hearing all of them against the original."
      ],
      "limits": [
        "A chain is a fixed set of decisions and does not adapt to the recording. A voice it does not suit sounds worse, which the comparison shows.",
        "Enhancement cannot recover detail a lossy encoder or a clipped preamp already discarded.",
        "Background noise is shaped by these filters but not removed; the noise reducer is the tool for that."
      ],
      "faq": [
        {
          "q": "Is the file uploaded?",
          "a": "No. Everything runs in this tab."
        },
        {
          "q": "Should I reduce noise before or after this?",
          "a": "Reduce noise first, then send the result straight into this tool with the button that makes a result the new source."
        },
        {
          "q": "Why does the phone chain sound thin?",
          "a": "It is meant to. It emulates a narrowband telephone path and is there for that effect."
        }
      ],
      "related": [
        "audio-denoise",
        "audio-dynamics",
        "audio-normalize"
      ]
    },
    {
      "slug": "audio-volume",
      "name": "Change volume",
      "category": "audio",
      "one_liner": "Raise or lower volume by an exact dB or a linear factor.",
      "replaces": "Volume sliders with no numbers behind them",
      "primitive": "Exact dB gain or linear level, applied in a single filter pass",
      "status": "done",
      "runner": "audiosound",
      "script": null,
      "tests": "4/6",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Change volume by an exact amount",
      "search_description": "Raise or lower level by a stated number of decibels or a linear factor, with an optional limiter, and see the change in the waveform. Nothing is uploaded.",
      "search_intro": "A volume change is the one audio edit with an exact answer, and it is still worth seeing. Raising the level of a recording that is already close to the ceiling clips it, and clipping is far easier to see in a waveform than to hear on the first pass. This page draws the source and the result on one shared scale, so the change is a visible change in height rather than two pictures that look the same because each was drawn to fit its own box.",
      "how_it_works": [
        "Choose an audio file. The waveform is drawn locally.",
        "Set the change as decibels of gain, or as a linear factor.",
        "Switch on the limiter if the level is going up and headroom is uncertain.",
        "Run, then compare the two waveforms and the two versions."
      ],
      "use_cases": [
        "Lifting a recording that was captured too quietly.",
        "Pulling a file down by an exact amount before mixing it with another.",
        "Seeing how close to the ceiling a recording already sits."
      ],
      "limits": [
        "Raising the level raises the noise with it; nothing here separates them.",
        "Without the limiter, a large increase clips. With it, the peaks are held back and the shape changes.",
        "A recording already clipped in the source is not repaired by turning it down."
      ],
      "faq": [
        {
          "q": "Is the file uploaded?",
          "a": "No. Everything runs in this tab."
        },
        {
          "q": "Should I use this or the loudness tool?",
          "a": "This applies an exact change chosen here. The loudness tool measures the recording and works out the change needed to reach a target."
        },
        {
          "q": "Why do both waveforms use the same scale?",
          "a": "So that a level change is visible. Drawing each one to fill its own box would hide exactly the thing being changed."
        }
      ],
      "related": [
        "audio-normalize",
        "audio-dynamics",
        "audio-compress"
      ]
    },
    {
      "slug": "batch-renamer",
      "name": "Batch renamer",
      "category": "image",
      "one_liner": "Rename files as a previewable validated plan: dry-run by default, two-phase cycle-safe apply, undo journal.",
      "replaces": "Bridge batch rename",
      "primitive": "Rename as a previewable plan (mandatory dry-run), not a blind find-replace",
      "status": "done",
      "runner": "imgtool",
      "script": null,
      "tests": "18/18",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "catalog-generator",
      "name": "Catalog generator",
      "category": "image",
      "one_liner": "Generate a catalog/price-sheet as data-driven output from a CSV (the data is the source of truth).",
      "replaces": "InDesign/Express catalog + price-sheet layout",
      "primitive": "Catalog as data-driven output from a CSV (the data is the source of truth), not a hand-laid document",
      "status": "done",
      "runner": "imgtool",
      "script": null,
      "tests": "11/11",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "cc-commonjs-to-esm",
      "name": "commonjs-to-esm",
      "category": "code",
      "one_liner": "Converts CommonJS modules to ESM: require() → import, module.exports → export, exports.x → export const x, __dirname/__filename → import.meta.url helpers.",
      "replaces": "Manual require/import rewrites and copy-paste markdown conversion",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "module/commonjs-to-esm.js",
      "tests": "1/1",
      "from": "commonjs",
      "to": "esm",
      "public": false
    },
    {
      "slug": "cc-cra-to-vite",
      "name": "cra-to-vite",
      "category": "code",
      "one_liner": "Migrates Create React App (CRA) projects to Vite. Generates vite.config.js, updates env vars (REACT_APP_ → VITE_), and suggests package.json script changes.",
      "replaces": "Tool migration guides followed by hand, file by file",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "cli",
      "script": null,
      "tests": "1/1",
      "from": "cra",
      "to": "vite",
      "public": false
    },
    {
      "slug": "cc-css-px-to-clamp",
      "name": "css-px-to-clamp",
      "category": "code",
      "one_liner": "Converts px font-size, padding, margin, and gap values to CSS clamp() for fluid responsive sizing. Uses viewport-width-based preferred value with rem min/max.",
      "replaces": "Tool migration guides followed by hand, file by file",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "config/css-px-to-clamp.js",
      "tests": "1/1",
      "from": "css-px",
      "to": "clamp",
      "public": false
    },
    {
      "slug": "cc-css-px-to-rem",
      "name": "css-px-to-rem",
      "category": "code",
      "one_liner": "Converts px units to rem in CSS. Preserves 0px as 0, optionally keeps 1px for borders, handles calc() expressions, var(), @media queries, @supports, and !important.",
      "replaces": "Tool migration guides followed by hand, file by file",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "config/css-px-to-rem.js",
      "tests": "1/1",
      "from": "css-px",
      "to": "rem",
      "public": false
    },
    {
      "slug": "cc-css-to-css-modules",
      "name": "css-to-css-modules",
      "category": "code",
      "one_liner": "Converts global CSS to CSS Modules format. Class selectors kept as-is (local by default). Element/tag and ID selectors converted to class equivalents with warnings. Global resets (html, body) wrapped in :global(). Generates optional JS import map with --with-map flag. Suggests moving @import rules to JS imports.",
      "replaces": "Manual CSS refactors and single-purpose style converter sites",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "css/css-to-css-modules.js",
      "tests": "1/1",
      "from": "css",
      "to": "css-modules",
      "public": false
    },
    {
      "slug": "cc-css-to-emotion",
      "name": "css-to-emotion",
      "category": "code",
      "one_liner": "Converts CSS rules to Emotion-compatible syntax. Supports css prop mode (--mode=prop) and styled mode (--mode=styled). Handles @media nesting, keyframes, and CSS variable → theme mapping.",
      "replaces": "Manual CSS refactors and single-purpose style converter sites",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "css/css-to-emotion.js",
      "tests": "1/1",
      "from": "css",
      "to": "emotion",
      "public": false
    },
    {
      "slug": "cc-css-to-scss",
      "name": "css-to-scss",
      "category": "code",
      "one_liner": "Converts flat CSS into nested SCSS by grouping related selectors, extracting repeated values to variables, and detecting BEM patterns. Simple nesting heuristics: shared prefix selectors get nested.",
      "replaces": "Manual CSS refactors and single-purpose style converter sites",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "css/css-to-scss.js",
      "tests": "1/1",
      "from": "css",
      "to": "scss",
      "public": false
    },
    {
      "slug": "cc-css-to-styled-components",
      "name": "css-to-styled-components",
      "category": "code",
      "one_liner": "Converts CSS rules to styled-components (React) syntax. Selector names become PascalCase component names. Nested selectors use & references or generate separate components.",
      "replaces": "Manual CSS refactors and single-purpose style converter sites",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "css/css-to-styled-components.js",
      "tests": "1/1",
      "from": "css",
      "to": "styled-components",
      "public": false
    },
    {
      "slug": "cc-css-to-tailwind",
      "name": "css-to-tailwind",
      "category": "code",
      "one_liner": "Converts CSS declarations to Tailwind CSS utility classes. Maps standard CSS properties to Tailwind utilities including colors, spacing, typography, layout, borders, and more. Outputs space-separated class strings or @apply blocks.",
      "replaces": "Manual CSS refactors and single-purpose style converter sites",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "css/css-to-tailwind.js",
      "tests": "1/1",
      "from": "css",
      "to": "tailwind",
      "public": false
    },
    {
      "slug": "cc-csv-to-json",
      "name": "csv-to-json",
      "category": "code",
      "one_liner": "Convert RFC 4180-style CSV text to formatted JSON through the CLI, browser workbench, and bounded local MCP. Preserve quoted fields and empty cells, report repaired rows, protect special object keys, and expose no arbitrary paths or flags.",
      "replaces": "Ad-hoc format massaging in spreadsheets and converter sites",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "data/csv-to-json.js",
      "tests": "6/6",
      "from": "csv",
      "to": "json",
      "public": false
    },
    {
      "slug": "cc-csv-to-sql",
      "name": "csv-to-sql",
      "category": "code",
      "one_liner": "Convert CSV data to SQL INSERT statements. Supports CREATE TABLE generation, type inference, batch inserts, and proper SQL escaping.",
      "replaces": "Ad-hoc format massaging in spreadsheets and converter sites",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "data/csv-to-sql.js",
      "tests": "1/1",
      "from": "csv",
      "to": "sql",
      "public": false
    },
    {
      "slug": "cc-csv-to-typescript",
      "name": "csv-to-typescript",
      "category": "code",
      "one_liner": "Convert CSV data to a TypeScript interface and typed const array. Infers types from data values and generates clean, exportable TypeScript code.",
      "replaces": "Ad-hoc format massaging in spreadsheets and converter sites",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "data/csv-to-typescript.js",
      "tests": "1/1",
      "from": "csv",
      "to": "typescript",
      "public": false
    },
    {
      "slug": "cc-curl-to-axios",
      "name": "curl-to-axios",
      "category": "code",
      "one_liner": "Convert cURL commands to Axios JavaScript code. Produces a ready-to-run script using axios with proper config for headers, body, auth, params, form data, file uploads, and redirect handling.",
      "replaces": "Hand-translating cURL snippets into client code",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "api/curl-to-axios.js",
      "tests": "1/1",
      "from": "curl",
      "to": "axios",
      "public": false
    },
    {
      "slug": "cc-curl-to-fetch",
      "name": "curl-to-fetch",
      "category": "code",
      "one_liner": "Convert cURL commands to JavaScript fetch() code. Handles headers, body data, JSON, form data, basic auth, cookies, file uploads, and insecure/redirect flags.",
      "replaces": "Hand-translating cURL snippets into client code",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "api/curl-to-fetch.js",
      "tests": "1/1",
      "from": "curl",
      "to": "fetch",
      "public": false
    },
    {
      "slug": "cc-curl-to-go",
      "name": "curl-to-go",
      "category": "code",
      "one_liner": "Convert cURL commands to Go net/http code. Generates idiomatic Go with http.NewRequest, proper header setting, strings.NewReader or bytes.NewBuffer for body, json.Marshal for JSON payloads, and the standard if err != nil error handling pattern.",
      "replaces": "Hand-translating cURL snippets into client code",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "api/curl-to-go.js",
      "tests": "1/1",
      "from": "curl",
      "to": "go",
      "public": false
    },
    {
      "slug": "cc-curl-to-php",
      "name": "curl-to-php",
      "category": "code",
      "one_liner": "Convert cURL commands to PHP code. By default generates native PHP cURL (curl_init/curl_setopt). Pass --guzzle to output Guzzle HTTP client code. Handles headers, body, JSON, form data, file uploads, basic auth, cookies, and SSL verification.",
      "replaces": "Hand-translating cURL snippets into client code",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "api/curl-to-php.js",
      "tests": "1/1",
      "from": "curl",
      "to": "php",
      "public": false
    },
    {
      "slug": "cc-curl-to-python",
      "name": "curl-to-python",
      "category": "code",
      "one_liner": "Convert cURL commands to Python requests code. Generates idiomatic Python using the requests library with proper dicts for headers, json= for JSON payloads, data= for form data, files= for uploads, and auth tuples for basic authentication.",
      "replaces": "Hand-translating cURL snippets into client code",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "api/curl-to-python.js",
      "tests": "1/1",
      "from": "curl",
      "to": "python",
      "public": false
    },
    {
      "slug": "cc-env-to-typed-config",
      "name": "env-to-typed-config",
      "category": "code",
      "one_liner": "Convert .env file to TypeScript config loader with validation. Generates a TypeScript interface, parse function, and validation with descriptive errors. Supports type coercion: PORT=number, ENABLED=boolean, DATABASE_URL=string. Includes Zod-style validation pattern using pure JS.",
      "replaces": "Ad-hoc format massaging in spreadsheets and converter sites",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "data/env-to-typed-config.js",
      "tests": "1/1",
      "from": "env",
      "to": "typed-config",
      "public": false
    },
    {
      "slug": "cc-esm-to-commonjs",
      "name": "esm-to-commonjs",
      "category": "code",
      "one_liner": "Converts ESM modules to CommonJS: import → require(), export → module.exports, default export → module.exports =, named exports → exports.x, import.meta.url → __filename.",
      "replaces": "Manual require/import rewrites and copy-paste markdown conversion",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "module/esm-to-commonjs.js",
      "tests": "1/1",
      "from": "esm",
      "to": "commonjs",
      "public": false
    },
    {
      "slug": "cc-eval-remover",
      "name": "eval-remover",
      "category": "code",
      "one_liner": "Finds and refactors eval() and new Function() calls to safer alternatives. Converts eval(\"...\") to JSON.parse where applicable, new Function() to inline arrow functions, and setTimeout/setInterval with strings to function wrappers. Adds warnings for cases requiring manual review.",
      "replaces": "Ad-hoc sanitizing regexes and unsafe copy-paste fixes",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "security/eval-remover.js",
      "tests": "1/1",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "cc-hardcoded-colors-to-tokens",
      "name": "hardcoded-colors-to-tokens",
      "category": "code",
      "one_liner": "Replaces hardcoded color values (hex, rgb, rgba, hsl, hsla) with design token references as CSS custom properties. Generates companion tokens file. Preserves alpha channels via rgba/hsla with token references.",
      "replaces": "Tool migration guides followed by hand, file by file",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "config/hardcoded-colors-to-tokens.js",
      "tests": "1/1",
      "from": "hardcoded-colors",
      "to": "tokens",
      "public": false
    },
    {
      "slug": "cc-html-to-jsx",
      "name": "html-to-jsx",
      "category": "code",
      "one_liner": "Converts HTML to JSX. Handles attributes (class→className, for→htmlFor, style strings→objects, event handlers→camelCase), self-closing tags, boolean attributes, HTML comments, and inline style camelCasing.",
      "replaces": "Framework rewrite chores and one-off online HTML/JSX converter sites",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "core/html-to-jsx.js",
      "tests": "1/1",
      "from": "html",
      "to": "jsx",
      "public": false
    },
    {
      "slug": "cc-html-to-markdown",
      "name": "html-to-markdown",
      "category": "code",
      "one_liner": "Converts HTML to Markdown. Handles headings, paragraphs, links, images, lists, code blocks, blockquotes, bold, italic, tables, and more. Strips unsupported attributes with warnings.",
      "replaces": "Manual require/import rewrites and copy-paste markdown conversion",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "module/html-to-markdown.js",
      "tests": "1/1",
      "from": "html",
      "to": "markdown",
      "public": false
    },
    {
      "slug": "cc-html-to-svelte",
      "name": "html-to-svelte",
      "category": "code",
      "one_liner": "Converts HTML to a Svelte component. Maps event handlers to Svelte's on:click syntax, adds <script> section with reactive variable hints, and wraps output in a .svelte scaffold.",
      "replaces": "Framework rewrite chores and one-off online HTML/JSX converter sites",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "core/html-to-svelte.js",
      "tests": "1/1",
      "from": "html",
      "to": "svelte",
      "public": false
    },
    {
      "slug": "cc-html-to-vue",
      "name": "html-to-vue",
      "category": "code",
      "one_liner": "Converts HTML to Vue SFC template syntax. Wraps content in a <template> block, converts common event and binding patterns to Vue directives (@click, :href, v-model), and sets up the SFC scaffold.",
      "replaces": "Framework rewrite chores and one-off online HTML/JSX converter sites",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "core/html-to-vue.js",
      "tests": "1/1",
      "from": "html",
      "to": "vue",
      "public": false
    },
    {
      "slug": "cc-inline-styles-to-css",
      "name": "inline-styles-to-css",
      "category": "code",
      "one_liner": "Extracts inline style=\"...\" attributes from HTML and generates CSS classes. Detects repeated style values to suggest shared classes. Replaces inline styles with class attributes. Generates class names based on element context or --prefix flag.",
      "replaces": "Manual CSS refactors and single-purpose style converter sites",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "css/inline-styles-to-css.js",
      "tests": "1/1",
      "from": "inline-styles",
      "to": "css",
      "public": false
    },
    {
      "slug": "cc-innerhtml-to-safe-render",
      "name": "innerhtml-to-safe-render",
      "category": "code",
      "one_liner": "Converts dangerous element.innerHTML assignments to safe DOM methods like textContent, setAttribute, or createElement/appendChild patterns. Detects when HTML is being set vs plain text and chooses the appropriate safe method.",
      "replaces": "Ad-hoc sanitizing regexes and unsafe copy-paste fixes",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "security/innerhtml-to-safe-render.js",
      "tests": "1/1",
      "from": "innerhtml",
      "to": "safe-render",
      "public": false
    },
    {
      "slug": "cc-jquery-to-vanilla",
      "name": "jquery-to-vanilla",
      "category": "code",
      "one_liner": "Converts jQuery patterns to vanilla JavaScript: selectors, DOM manipulation, events, AJAX, utilities, and chained calls to native equivalents.",
      "replaces": "Manual require/import rewrites and copy-paste markdown conversion",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "module/jquery-to-vanilla.js",
      "tests": "1/1",
      "from": "jquery",
      "to": "vanilla",
      "public": false
    },
    {
      "slug": "cc-js-to-ts",
      "name": "js-to-ts",
      "category": "code",
      "one_liner": "Adds basic TypeScript annotations to JavaScript code. Infers types from JSDoc @param/@returns, default values, object shapes, and homogeneous arrays. Converts require() to import, adds .ts extensions.",
      "replaces": "Framework rewrite chores and one-off online HTML/JSX converter sites",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "core/js-to-ts.js",
      "tests": "1/1",
      "from": "js",
      "to": "ts",
      "public": false
    },
    {
      "slug": "cc-json-schema-to-typescript",
      "name": "json-schema-to-typescript",
      "category": "code",
      "one_liner": "Converts JSON Schema (draft-07) to TypeScript interfaces. Handles: type, enum, anyOf, oneOf, allOf, $ref, array items, object properties, required fields, nested definitions.",
      "replaces": "Hand-written types and schemas that drift from the data they describe",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "schema/json-schema-to-typescript.js",
      "tests": "1/1",
      "from": "json-schema",
      "to": "typescript",
      "public": false
    },
    {
      "slug": "cc-json-schema-to-zod",
      "name": "json-schema-to-zod",
      "category": "code",
      "one_liner": "Converts JSON Schema (draft-07) to Zod schema definitions. Handles: type, enum, anyOf, oneOf, allOf, $ref, array items, object properties, required fields, string formats, nested definitions.",
      "replaces": "Hand-written types and schemas that drift from the data they describe",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "schema/json-schema-to-zod.js",
      "tests": "1/1",
      "from": "json-schema",
      "to": "zod",
      "public": false
    },
    {
      "slug": "cc-json-to-json-schema",
      "name": "json-to-json-schema",
      "category": "code",
      "one_liner": "Converts JSON data into JSON Schema (draft-07) with intelligent type inference. Adds human-readable descriptions derived from key names, handles nested objects, arrays, null values, and optional field detection.",
      "replaces": "Hand-written types and schemas that drift from the data they describe",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "schema/json-to-json-schema.js",
      "tests": "1/1",
      "from": "json",
      "to": "json-schema",
      "public": false
    },
    {
      "slug": "cc-json-to-typescript",
      "name": "json-to-typescript",
      "category": "code",
      "one_liner": "Convert representative JSON text to valid TypeScript declarations through the CLI, browser workbench, and bounded local MCP. Preserve explicit root names, top-level scalars and arrays, unsafe keys, heterogeneous types, and collision-safe nested structure.",
      "replaces": "Hand-written types and schemas that drift from the data they describe",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "schema/json-to-typescript.js",
      "tests": "6/6",
      "from": "json",
      "to": "typescript",
      "public": false
    },
    {
      "slug": "cc-json-to-yaml",
      "name": "json-to-yaml",
      "category": "code",
      "one_liner": "Convert JSON text to deterministic valid YAML through the CLI, browser workbench, and bounded local MCP. Preserve nested structure and scalar meaning for ambiguous, multiline, controlled, and Unicode strings.",
      "replaces": "Ad-hoc format massaging in spreadsheets and converter sites",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "data/json-to-yaml.js",
      "tests": "6/6",
      "from": "json",
      "to": "yaml",
      "public": false
    },
    {
      "slug": "cc-json-to-zod",
      "name": "json-to-zod",
      "category": "code",
      "one_liner": "Converts JSON data into Zod schema definitions with intelligent inference. Adds .optional() for missing keys, .nullable() for nulls, .email()/.url()/ .datetime() heuristics, and handles nested objects & arrays.",
      "replaces": "Hand-written types and schemas that drift from the data they describe",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "schema/json-to-zod.js",
      "tests": "1/1",
      "from": "json",
      "to": "zod",
      "public": false
    },
    {
      "slug": "cc-jsx-to-html",
      "name": "jsx-to-html",
      "category": "code",
      "one_liner": "Converts JSX back to static HTML. Reverses JSX attribute naming (className→class, htmlFor→for), converts style objects to strings, removes event handlers with warning comments, and strips JSX expressions.",
      "replaces": "Framework rewrite chores and one-off online HTML/JSX converter sites",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "core/jsx-to-html.js",
      "tests": "1/1",
      "from": "jsx",
      "to": "html",
      "public": false
    },
    {
      "slug": "cc-lodash-to-native",
      "name": "lodash-to-native",
      "category": "code",
      "one_liner": "Converts Lodash usage to native JavaScript ES2015+ equivalents. Handles 30+ methods with per-method mapping, optional chaining, and fallbacks.",
      "replaces": "Tool migration guides followed by hand, file by file",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "config/lodash-to-native.js",
      "tests": "1/1",
      "from": "lodash",
      "to": "native",
      "public": false
    },
    {
      "slug": "cc-markdown-to-html",
      "name": "markdown-to-html",
      "category": "code",
      "one_liner": "Converts Markdown to HTML. Supports headings, bold, italic, code, code blocks, links, images, blockquotes, lists, horizontal rules, tables, line breaks, and strikethrough. No external dependencies.",
      "replaces": "Manual require/import rewrites and copy-paste markdown conversion",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "module/markdown-to-html.js",
      "tests": "1/1",
      "from": "markdown",
      "to": "html",
      "public": false
    },
    {
      "slug": "cc-minified-to-readable",
      "name": "minified-to-readable",
      "category": "code",
      "one_liner": "Formats minified JavaScript, HTML, or CSS into readable, indented code. Handles proper indentation, brace placement, comment preservation, and optional attribute/property alignment.",
      "replaces": "Ad-hoc sanitizing regexes and unsafe copy-paste fixes",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "security/minified-to-readable.js",
      "tests": "1/1",
      "from": "minified",
      "to": "readable",
      "public": false
    },
    {
      "slug": "cc-moment-to-date-fns",
      "name": "moment-to-date-fns",
      "category": "code",
      "one_liner": "Converts Moment.js code to date-fns. Handles format, add, subtract, diff, startOf, endOf, isBefore, isAfter, isSame, fromNow, and more.",
      "replaces": "Tool migration guides followed by hand, file by file",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "config/moment-to-date-fns.js",
      "tests": "1/1",
      "from": "moment",
      "to": "date-fns",
      "public": false
    },
    {
      "slug": "cc-openapi-to-markdown",
      "name": "openapi-to-markdown",
      "category": "code",
      "one_liner": "Converts OpenAPI 3.x specifications (JSON or YAML) to comprehensive Markdown documentation. Generates endpoint sections with method badges, parameter tables, request body examples, and response documentation.",
      "replaces": "Tool migration guides followed by hand, file by file",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "config/openapi-to-markdown.js",
      "tests": "1/1",
      "from": "openapi",
      "to": "markdown",
      "public": false
    },
    {
      "slug": "cc-pretty-to-minified",
      "name": "pretty-to-minified",
      "category": "code",
      "one_liner": "Minifies JavaScript, CSS, or HTML by removing whitespace, comments, and optional variable name mangling.",
      "replaces": "Ad-hoc sanitizing regexes and unsafe copy-paste fixes",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "security/pretty-to-minified.js",
      "tests": "1/1",
      "from": "pretty",
      "to": "minified",
      "public": false
    },
    {
      "slug": "cc-react-class-to-function",
      "name": "react-class-to-function",
      "category": "code",
      "one_liner": "Converts React class components to function components with hooks. Handles constructor→useState, componentDidMount→useEffect, this.state→useState, this.props→props parameter, render() extraction, componentWillUnmount cleanup, and getDerivedStateFromProps warnings.",
      "replaces": "Framework rewrite chores and one-off online HTML/JSX converter sites",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "core/react-class-to-function.js",
      "tests": "1/1",
      "from": "react-class",
      "to": "function",
      "public": false
    },
    {
      "slug": "cc-sass-variables-to-css-vars",
      "name": "sass-variables-to-css-vars",
      "category": "code",
      "one_liner": "Converts Sass/SCSS $variables to CSS custom properties. Moves definitions to a :root block, replaces $var usage with var(--var). Handles #{$var} interpolation, !default flags, and computed expressions.",
      "replaces": "Tool migration guides followed by hand, file by file",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "config/sass-variables-to-css-vars.js",
      "tests": "1/1",
      "from": "sass-variables",
      "to": "css-vars",
      "public": false
    },
    {
      "slug": "cc-scss-to-css",
      "name": "scss-to-css",
      "category": "code",
      "one_liner": "Flattens SCSS into plain CSS by resolving nesting, parent selectors (&), variables, mixins, imports, and control directives. Outputs flat selectors with BEM-style expansion for & variants.",
      "replaces": "Manual CSS refactors and single-purpose style converter sites",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "css/scss-to-css.js",
      "tests": "1/1",
      "from": "scss",
      "to": "css",
      "public": false
    },
    {
      "slug": "cc-sql-schema-to-typescript",
      "name": "sql-schema-to-typescript",
      "category": "code",
      "one_liner": "Converts SQL CREATE TABLE statements to TypeScript interfaces. Parses columns, types, constraints, primary keys, foreign keys, and nullability.",
      "replaces": "Tool migration guides followed by hand, file by file",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "config/sql-schema-to-typescript.js",
      "tests": "1/1",
      "from": "sql-schema",
      "to": "typescript",
      "public": false
    },
    {
      "slug": "cc-sql-to-parameterized",
      "name": "sql-to-parameterized",
      "category": "code",
      "one_liner": "Converts SQL queries with string concatenation/interpolation into parameterized queries with placeholder values (? or $1, $2, etc.) and a separate parameters array. Supports multiple SQL dialects.",
      "replaces": "Ad-hoc sanitizing regexes and unsafe copy-paste fixes",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "security/sql-to-parameterized.js",
      "tests": "1/1",
      "from": "sql",
      "to": "parameterized",
      "public": false
    },
    {
      "slug": "cc-static-values-to-css-vars",
      "name": "static-values-to-css-vars",
      "category": "code",
      "one_liner": "Extracts repeated CSS values (colors, font-sizes, spacing, border-radius) and replaces them with CSS custom properties in a :root block. Values appearing >= --threshold times are extracted. Smart naming based on value type.",
      "replaces": "Tool migration guides followed by hand, file by file",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "config/static-values-to-css-vars.js",
      "tests": "1/1",
      "from": "static-values",
      "to": "css-vars",
      "public": false
    },
    {
      "slug": "cc-svg-sanitizer",
      "name": "svg-sanitizer",
      "category": "code",
      "one_liner": "Sanitizes SVG files by removing dangerous content including script tags, javascript: handlers, foreignObject, XSL processing instructions, event handlers, and external resource references. Preserves pure vector graphics, gradients, filters, and SMIL animations.",
      "replaces": "Ad-hoc sanitizing regexes and unsafe copy-paste fixes",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "security/svg-sanitizer.js",
      "tests": "1/1",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "cc-svg-to-css-background",
      "name": "svg-to-css-background",
      "category": "code",
      "one_liner": "Convert SVG to CSS background-image data URI. Supports class-name customization, sizing, and color replacement (e.g., currentColor → #f00).",
      "replaces": "Hand-porting SVG markup into framework components",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "svg/svg-to-css-background.js",
      "tests": "1/1",
      "from": "svg",
      "to": "css-background",
      "public": false
    },
    {
      "slug": "cc-svg-to-html",
      "name": "svg-to-html",
      "category": "code",
      "one_liner": "Embed SVG inline into HTML with proper sizing. Supports responsive scaling (--responsive) and SVG sprite preparation (--sprite).",
      "replaces": "Hand-porting SVG markup into framework components",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "svg/svg-to-html.js",
      "tests": "1/1",
      "from": "svg",
      "to": "html",
      "public": false
    },
    {
      "slug": "cc-svg-to-react",
      "name": "svg-to-react",
      "category": "code",
      "one_liner": "Convert SVG to React JSX component with camelCased attributes, style-object conversion, optional TypeScript types, and automatic component-name detection.",
      "replaces": "Hand-porting SVG markup into framework components",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "svg/svg-to-react.js",
      "tests": "1/1",
      "from": "svg",
      "to": "react",
      "public": false
    },
    {
      "slug": "cc-svg-to-svelte",
      "name": "svg-to-svelte",
      "category": "code",
      "one_liner": "Convert SVG to Svelte component with `export let` prop declarations, class directive support, and event forwarding.",
      "replaces": "Hand-porting SVG markup into framework components",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "svg/svg-to-svelte.js",
      "tests": "1/1",
      "from": "svg",
      "to": "svelte",
      "public": false
    },
    {
      "slug": "cc-svg-to-vue",
      "name": "svg-to-vue",
      "category": "code",
      "one_liner": "Convert SVG to Vue 3 Single-File Component (SFC) with configurable props for width, height, fill, and other attributes.",
      "replaces": "Hand-porting SVG markup into framework components",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "svg/svg-to-vue.js",
      "tests": "1/1",
      "from": "svg",
      "to": "vue",
      "public": false
    },
    {
      "slug": "cc-tailwind-to-css",
      "name": "tailwind-to-css",
      "category": "code",
      "one_liner": "Converts Tailwind CSS utility classes to standard CSS declarations. Parses responsive prefixes (sm:, md:, lg:, xl:), state modifiers (hover:, focus:, active:), and arbitrary values (e.g., m-[17px]). Outputs grouped CSS rules with @media wrappers.",
      "replaces": "Manual CSS refactors and single-purpose style converter sites",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "css/tailwind-to-css.js",
      "tests": "1/1",
      "from": "tailwind",
      "to": "css",
      "public": false
    },
    {
      "slug": "cc-toml-to-json",
      "name": "toml-to-json",
      "category": "code",
      "one_liner": "Convert TOML to JSON. Handles key=value pairs, [sections], arrays, inline tables, strings (including multiline \"\"\" \"\"\"), numbers, booleans, dates, and dotted keys (flattened or nested).",
      "replaces": "Ad-hoc format massaging in spreadsheets and converter sites",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "data/toml-to-json.js",
      "tests": "1/1",
      "from": "toml",
      "to": "json",
      "public": false
    },
    {
      "slug": "cc-ts-to-js",
      "name": "ts-to-js",
      "category": "code",
      "one_liner": "Strips all TypeScript syntax from code. Removes type annotations, interfaces, type aliases, generics, 'as' casts, 'satisfies' operator, converts enums to plain objects, and handles 'declare module' / triple-slash directives. Preserves ESM imports/exports and removes .ts extensions.",
      "replaces": "Framework rewrite chores and one-off online HTML/JSX converter sites",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "core/ts-to-js.js",
      "tests": "1/1",
      "from": "ts",
      "to": "js",
      "public": false
    },
    {
      "slug": "cc-ts-to-json-schema",
      "name": "ts-to-json-schema",
      "category": "code",
      "one_liner": "Converts TypeScript interfaces and type aliases to JSON Schema (draft-07). Parses: interface, type alias, enum, arrays, unions, optional (?), Record, generics (with warning), nested objects, primitive types.",
      "replaces": "Hand-written types and schemas that drift from the data they describe",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "schema/ts-to-json-schema.js",
      "tests": "1/1",
      "from": "ts",
      "to": "json-schema",
      "public": false
    },
    {
      "slug": "cc-ts-to-zod",
      "name": "ts-to-zod",
      "category": "code",
      "one_liner": "Converts TypeScript interfaces and type aliases to Zod schema definitions. Parses: interface, type alias, enum, arrays, unions, optional (?), Record, generics (with warning), nested objects. Complex conditional types → warn and use z.any().",
      "replaces": "Hand-written types and schemas that drift from the data they describe",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "schema/ts-to-zod.js",
      "tests": "1/1",
      "from": "ts",
      "to": "zod",
      "public": false
    },
    {
      "slug": "cc-tsconfig-fixer",
      "name": "tsconfig-fixer",
      "category": "code",
      "one_liner": "Analyzes and repairs tsconfig.json files. Validates against JSON Schema, adds missing required fields, fixes deprecated options, and suggests strict settings.",
      "replaces": "Tool migration guides followed by hand, file by file",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "config/tsconfig-fixer.js",
      "tests": "1/1",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "cc-unsafe-html-sanitizer",
      "name": "unsafe-html-sanitizer",
      "category": "code",
      "one_liner": "Sanitizes HTML by removing dangerous content including script tags, javascript: URLs, event handlers, objects, embeds, iframes, and forms. Produces safe HTML for rendering untrusted content.",
      "replaces": "Ad-hoc sanitizing regexes and unsafe copy-paste fixes",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "security/unsafe-html-sanitizer.js",
      "tests": "1/1",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "cc-webpack-to-vite",
      "name": "webpack-to-vite",
      "category": "code",
      "one_liner": "Converts webpack configuration files to Vite configuration files. Handles entry, output, resolve aliases, devServer, and common plugins.",
      "replaces": "Tool migration guides followed by hand, file by file",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "config/webpack-to-vite.js",
      "tests": "1/1",
      "from": "webpack",
      "to": "vite",
      "public": false
    },
    {
      "slug": "cc-yaml-to-json",
      "name": "yaml-to-json",
      "category": "code",
      "one_liner": "Convert YAML to JSON. Handles scalars, sequences, mappings, nested structures, comments, quoted strings, and multiline strings (literal | and folded >). No external dependencies - pure Node.js YAML parser.",
      "replaces": "Ad-hoc format massaging in spreadsheets and converter sites",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "data/yaml-to-json.js",
      "tests": "1/1",
      "from": "yaml",
      "to": "json",
      "public": false
    },
    {
      "slug": "cc-zod-to-json-schema",
      "name": "zod-to-json-schema",
      "category": "code",
      "one_liner": "Converts Zod schema code to JSON Schema (draft-07). Handles: z.string(), z.number(), z.boolean(), z.array(), z.object(), z.enum(), z.union(), z.optional(), z.nullable(), z.literal(), z.record(), z.tuple(), z.date(), z.any(), z.unknown(), z.null(), z.undefined().",
      "replaces": "Hand-written types and schemas that drift from the data they describe",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "schema/zod-to-json-schema.js",
      "tests": "1/1",
      "from": "zod",
      "to": "json-schema",
      "public": false
    },
    {
      "slug": "cc-zod-to-typescript",
      "name": "zod-to-typescript",
      "category": "code",
      "one_liner": "Parses Zod schema code and extracts TypeScript type definitions. Handles: z.string(), z.number(), z.boolean(), z.array(), z.object(), z.enum(), z.union(), z.optional(), z.nullable(), z.literal(), z.record(), z.tuple(), z.date(), z.any(), z.unknown(), z.null(), z.undefined().",
      "replaces": "Hand-written types and schemas that drift from the data they describe",
      "primitive": "Deterministic single-purpose text transform: a zero-dependency Node script, stdin/stdout pipeable, that suggests the right sibling tool on mismatched input",
      "status": "done",
      "runner": "converter",
      "script": "schema/zod-to-typescript.js",
      "tests": "1/1",
      "from": "zod",
      "to": "typescript",
      "public": false
    },
    {
      "slug": "cheat-sheet-converter",
      "name": "PDF/document -> clean output converter",
      "category": "other",
      "one_liner": "PDF/document converter with three intended output modes: replica mode for exact visual fidelity, hybrid mode for semantic regions over preserved visuals, and reflow mode for mobile/LMS-ready web-native content. The core product primitive is the region tree: blocks, figures, diagrams, reading order, bounding boxes, and confidence.",
      "replaces": "Acrobat PDF export (PDF->HTML/Word/etc.)",
      "primitive": "visual structure first, semantic document second",
      "status": "done",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "contact-sheet",
      "name": "Contact sheet",
      "category": "image",
      "one_liner": "Lay a set of images out as a print artifact: paper size at a chosen resolution, captioned frames, pages numbered and spilled automatically, every page on screen before it is written.",
      "replaces": "Bridge contact sheet",
      "primitive": "A one-command proof sheet for reviewing a set of images",
      "status": "done",
      "runner": "imagelayout",
      "script": null,
      "tests": "574/574",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Contact sheet maker with real paper sizes and page numbers",
      "search_description": "Turn a shoot into printable proof sheets at A4 or Letter, at a resolution you choose, paginated automatically with captioned frames. Every page is on screen before you download it. Runs locally, nothing is uploaded.",
      "search_intro": "A proof sheet is a printing artifact before it is an image. Its width is a sheet of paper, its resolution decides whether a caption survives the printer, and a shoot of two hundred frames becomes a numbered sequence of pages rather than one enormous picture. Most browser tools that call themselves contact sheet makers skip all three: they hand back a single image at whatever pixel size the grid happened to need, with no paper behind it and nowhere for frame 61 to go. This one starts from the sheet. Choose A4 or Letter and a resolution between 36 and 1200, and the margins, gutters, cell width, caption strip and rows per sheet are all derived from that paper at that resolution, exactly as the command line tool derives them. Frames that do not fit flow onto the next sheet and every sheet is numbered in its heading. The preview is the sheet, drawn at the same geometry as the file you download, so what you approve on screen is the arrangement that prints.",
      "how_it_works": [
        "Choose frames. They are decoded in this tab and listed in the order they will be laid out. A frame the browser cannot decode is named in that list and left out of the sheet rather than ending the run.",
        "Set the order. Digit-aware ordering is the default, so frame 2 comes before frame 10 rather than after it, and the arrows beside any frame move it by hand.",
        "Set the paper. Paper size, resolution, columns, background, heading and captions each redraw the sheet immediately. When the frames exceed one sheet, a page control appears and every sheet can be inspected.",
        "Download. A single sheet arrives as one image and a paginated run arrives as a numbered set, both rendered at full paper resolution rather than at preview resolution."
      ],
      "use_cases": [
        "Hand a client a printed page of every frame from a session, numbered, so selections can be marked with a pen.",
        "Keep a printed index beside an archive of negatives, scans or exported masters.",
        "Check a folder of exports at a glance before delivery, at the size they will actually be printed."
      ],
      "limits": [
        "The sheet is a raster page. It is not a PDF and it carries no vector text.",
        "A sheet is refused above 16384 pixels on a side or 40 megapixels in total, because a canvas beyond that quietly paints nothing. Fewer columns, a lower resolution or a paper size instead of none will bring it back inside.",
        "Captions are the file names. There is no field for a title, a rating or a keyword taken from the frame.",
        "Ordering by capture time is not offered here; the order is the one shown in the list."
      ],
      "faq": [
        {
          "q": "Why does the resolution matter if I only want to look at it?",
          "a": "It decides how many pixels a printed inch gets. At 150 the captions stay readable on paper; at 72 they blur. If the sheet is only ever going on a screen, choose the smaller number and the file gets smaller with it."
        },
        {
          "q": "What happens to a frame the browser cannot open?",
          "a": "It is named in the frame list and left out of the sheet. The count under the list always says how many were laid out and how many were excluded."
        },
        {
          "q": "Can I get one long sheet instead of pages?",
          "a": "Choose none for the paper and the sheet grows to fit every frame in one column run, up to the size limit."
        }
      ],
      "related": [
        "gallery-export",
        "image-resizer",
        "dupe-finder"
      ]
    },
    {
      "slug": "dupe-finder",
      "name": "Dupe finder",
      "category": "image",
      "one_liner": "Fingerprint images locally, group every near-copy beside the keeper the stated rule chose, decide which copies to discard, and leave with a written receipt. Nothing is uploaded and nothing is deleted.",
      "replaces": "Lightroom duplicate detection",
      "primitive": "Focused find-the-dupes with an adjustable similarity threshold, not a whole catalog system",
      "status": "done",
      "runner": "imgtool",
      "script": null,
      "tests": "21/21",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Find duplicate photos and decide which copy to keep",
      "search_description": "Fingerprint a folder of pictures in this tab, see every near-copy grouped beside the one worth keeping, mark the rest yourself, and leave with a written decision list. Nothing is uploaded and nothing is deleted.",
      "search_intro": "A duplicate cleaner earns trust by showing its working, because the mistake it can make is permanent. The awkward part is that a fingerprint distance of 4 and a distance of 9 look equally authoritative in a list of filenames, and only one of them is worth acting on. This board never asks anyone to act on a number alone. Each picture that was fingerprinted appears inside the group it landed in, at a size where the difference between a second export and a different photograph is visible. The member the keeper rule chose sits in that group locked, because a review that can end with every copy marked for removal is a trap rather than a review. Every other member carries its own distance from that keeper and the width of the fingerprint it was measured across, so a distance can be read as the fraction of the signature it actually is. The fingerprint itself is the one the local command line tool computes, bit for bit, so a decision reached here and a decision reached in a terminal are the same decision.",
      "how_it_works": [
        "Add the pictures. Each one is read at its own resolution inside this tab, reduced to a 128 bit fingerprint, and listed with its dimensions and its size on disk.",
        "Read the groups. Byte identical files come first as their own kind of group, then the sets the fingerprint pulled together, each showing the widest distance inside it and what that distance is worth.",
        "Set the distance. Zero means the fingerprints agree exactly, one to two means the same picture, and the published account of this fingerprint reports visible false matches from four upward, so a wider setting is an invitation to look rather than a verdict.",
        "Mark what to discard, then download the receipt. It names every group, its keeper, each member's distance, and the count on each side of the decision."
      ],
      "use_cases": [
        "Work out which of five nearly identical exports of one photograph is the full size original before clearing space.",
        "Check a shared folder for pictures that were sent twice under different names, without handing the folder to a service.",
        "Produce a written list of what to remove that somebody else can check before anything is removed."
      ],
      "limits": [
        "Nothing on disk is touched. This board reads and decides; removing or moving files is what the local tool with its move option is for.",
        "A fingerprint describes brightness gradients, so it matches a resave, a rescale and a mild edit, and it does not match a crop, a flip or a rotation. Those read as different pictures.",
        "Two unrelated photographs can land within eight bits of each other. That is why the distance is shown per member and why the wide bands are labelled as questions.",
        "Colour is discarded before the fingerprint is taken, so two frames identical in shape but different in colour read as the same picture.",
        "One picture is capped at forty megapixels, and each file is read one at a time so a large folder stays inside the tab's memory.",
        "Raw camera files, video and anything the browser cannot decode are listed as unreadable rather than silently skipped."
      ],
      "faq": [
        {
          "q": "Are the pictures uploaded?",
          "a": "No. They are read, fingerprinted and grouped inside this tab. Nothing leaves the browser and the page keeps working with the network off."
        },
        {
          "q": "Can this delete the duplicates?",
          "a": "No, by design. A web page cannot be given the power to delete a photo library, and it should not be. The receipt lists exactly what was chosen so the local tool, or a person, can act on it."
        },
        {
          "q": "What decides which copy is the keeper?",
          "a": "The largest picture, then the largest file, then the shortest name. That member is locked in every group so a review can never end with nothing kept."
        },
        {
          "q": "What does a distance of 6 mean?",
          "a": "Six of the fingerprint's 128 bits differ. It is inside the range where different photographs sometimes land, so it means look at the two pictures, not act on the number."
        },
        {
          "q": "Does it match a cropped or rotated copy?",
          "a": "No. The fingerprint is taken over the whole frame, so moving or turning the frame changes it. A crop of twenty pixels already reads as a different picture."
        }
      ],
      "related": [
        "image-diff",
        "image-optimizer",
        "contact-sheet"
      ]
    },
    {
      "slug": "dynamic_diagonal_watermark",
      "name": "Diagonal personalized watermark",
      "category": "other",
      "one_liner": "[mt4] Adaptive diagonal text grid personalized per buyer. First draft 2026-07-10; audited.",
      "replaces": "Adobe/Acrobat",
      "primitive": "Per-copy diagonal that adapts to page size.",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "favicon-generator",
      "name": "Favicon Generator",
      "category": "image",
      "one_liner": "Turn one local raster source into a bounded favicon and PWA launch package with ordinary and maskable icons, ICO, manifest, install snippet, context review, ZIP, and versioned receipt. Publishing, crawler access, search appearance, real PWA installation, and exact platform rendering are outside scope.",
      "replaces": "Single-size favicon makers and icon export flows that omit manifest, maskable, placement, and review evidence",
      "primitive": "Complete local deployment bundle with decoded real-context previews, separate ordinary and maskable purposes, safe-zone review, install markup, and a current-model receipt",
      "status": "done",
      "runner": "imgtool",
      "script": null,
      "tests": "14/14",
      "from": null,
      "to": null,
      "public": false,
      "search_title": "Favicon Generator with Live Context Preview | Private",
      "search_description": "Build a complete favicon, Apple touch icon, maskable PWA icon, manifest, and install snippet locally. Preview browser, search, and device crops before export.",
      "search_intro": "A favicon is not one tiny square. The same artwork is reduced into browser tabs, search results, Apple touch icons, and Android masks, each with different cropping pressure. This workbench generates the actual files first, then previews those files in representative contexts so unsafe padding, weak contrast, and missing manifest fields can be caught before launch.",
      "how_it_works": [
        "Choose one source image and define the app name, launch path, scope, colors, and standard padding.",
        "Generate raster icons, a multi-size ICO, a separately declared maskable icon, the web manifest, and the install snippet in this tab.",
        "Review browser, search, Apple touch, and circular Android contexts, then download individual files or a complete ZIP with a versioned receipt."
      ],
      "use_cases": [
        "Prepare a consistent launch icon package for a website or progressive web app.",
        "Check whether logo detail survives at 32 and 48 pixels before deployment.",
        "Keep maskable artwork inside the recommended safe zone while preserving a separate ordinary icon."
      ],
      "limits": [
        "Context previews are representative; browsers, operating systems, launchers, and search engines can render or crop icons differently.",
        "Search engines decide whether to display a favicon and do not guarantee that a submitted icon will appear.",
        "The tool generates files and installation markup but does not publish them or verify that a live site's crawler can reach them."
      ],
      "faq": [
        {
          "q": "Are source images uploaded?",
          "a": "No. Decoding, resizing, previews, manifest creation, ZIP packaging, and downloads happen locally in this browser tab."
        },
        {
          "q": "Why is the maskable icon separate?",
          "a": "An icon declared as maskable may be cropped into many launcher shapes. This package gives it a protected inner safe zone instead of falsely declaring the ordinary full-bleed icon as safe."
        },
        {
          "q": "What gets downloaded?",
          "a": "The package includes 16, 32, 48, 180, 192, and 512 pixel PNGs, a 512 pixel maskable PNG, a multi-size ICO, a web manifest, an HTML snippet, and a versioned receipt."
        }
      ],
      "related": [
        "image-optimizer",
        "image-resizer",
        "image-diff"
      ]
    },
    {
      "slug": "flashcard_maker",
      "name": "Flashcard maker",
      "category": "other",
      "one_liner": "[mt4] First draft returned 2026-07-10; audited by irp-o.",
      "replaces": "Adobe/Acrobat flashcard export",
      "primitive": "extraction + Q/A generation",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "forumala-translator",
      "name": "Formula translation tool",
      "category": "other",
      "one_liner": "Formula/notation conversion preserving semantic formula structure. Precedent proof.",
      "replaces": "generic formula/notation conversion",
      "primitive": "semantic formula structure",
      "status": "done",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "gallery-export",
      "name": "Gallery export",
      "category": "image",
      "one_liner": "Turn a folder of images into a small static site you own, shown running in the page before it is downloaded: thumbnails, captions and a working lightbox in plain HTML, CSS and JavaScript.",
      "replaces": "Lightroom/Bridge web-gallery export",
      "primitive": "Gallery as a shippable static site you own (HTML/CSS/JS + lightbox), not a proprietary export",
      "status": "done",
      "runner": "imagelayout",
      "script": null,
      "tests": "574/574",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Build a static photo gallery website from a folder in the browser",
      "search_description": "Turn a folder of pictures into a small website you own: thumbnails, captions and a working lightbox in plain HTML, CSS and JavaScript, ready to upload anywhere. No account, no install, nothing leaves the tab.",
      "search_intro": "The tools that build a static gallery site are almost all command line programs. They want a package manager, a container or an interpreter before they will make you a web page, which rules them out for anybody who simply has a folder of pictures and somewhere to put them. This does the same job in a tab. It writes an index page, a stylesheet, a few lines of dependency-free script, a full-size copy of every picture and a thumbnail for each, and hands the whole folder back as an archive that will run from any static host, any object store, or a memory stick with no server at all. Nothing about it phones home and no part of it needs to stay online for the gallery to keep working. The gallery itself is shown running before you download it, in a frame carrying the same markup, the same stylesheet and the same script that go into the archive, so the site you approve is the site you get.",
      "how_it_works": [
        "Choose pictures. They are read in this tab, listed in the order visitors will meet them, and any file that cannot be decoded is named in that list instead of stopping the build.",
        "Arrange them. Digit-aware ordering is the default so picture 2 precedes picture 10, and the arrows beside any entry move it by hand.",
        "Set the title, the column count and the thumbnail edge. Each change rebuilds the site in the frame beside the list, thumbnails and all.",
        "Try it, then download. Open a picture in the frame the way a visitor would, close it again, and take the archive when it behaves. Upload the contents to any static host."
      ],
      "use_cases": [
        "Publish a set of pictures on your own domain without handing them to a social platform or a gallery service.",
        "Send somebody a self-contained folder they can open from disk, with no server and no internet connection.",
        "Keep a permanent, dependency-free record of a collection that will still open in a browser in ten years."
      ],
      "limits": [
        "Full-size pictures are copied as they are. They are not re-encoded, so the archive is at least as large as the originals.",
        "One flat gallery per build. Sub-folders do not become albums and there are no tags or filters.",
        "Captions are file names. There is no field for a description or a credit.",
        "Video is not handled."
      ],
      "faq": [
        {
          "q": "Where can I host what this produces?",
          "a": "Anywhere that serves files. It is plain HTML, CSS and JavaScript with no build step and no server code, so a static host, an object store or a folder opened from disk all work."
        },
        {
          "q": "Does the gallery need this site to keep working?",
          "a": "No. Once the archive is unpacked it is self-contained and has no link back here."
        },
        {
          "q": "Can a visitor open a picture with the keyboard?",
          "a": "Yes. Each thumbnail is reachable by tabbing, Enter opens it, the arrow keys move between pictures and Escape closes the viewer and returns to the thumbnail that was open."
        }
      ],
      "related": [
        "contact-sheet",
        "image-optimizer",
        "image-resizer"
      ]
    },
    {
      "slug": "icon-set-exporter",
      "name": "Icon Set Exporter",
      "category": "other",
      "one_liner": "[mt6] First draft returned 2026-07-10; audited by irp-o.",
      "replaces": "Export-for-screens dialogs",
      "primitive": "one source -> explicit size/format matrix",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "image-export-matrix",
      "name": "Image Export Matrix",
      "category": "other",
      "one_liner": "[mt6] First draft returned 2026-07-10; audited by irp-o.",
      "replaces": "Export As dialogs, one at a time",
      "primitive": "the variant matrix spec (sizes x formats x quality) as the unit",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "image-levels",
      "name": "Image levels",
      "category": "image",
      "one_liner": "Apply a reproducible, versionable JSON tone recipe (levels/curves) to images in batch.",
      "replaces": "Lightroom/Photoshop levels+curves (per-image slider session)",
      "primitive": "A reproducible, versionable tone recipe (JSON) applied in batch, not a manual slider pass",
      "status": "done",
      "runner": "imgtool",
      "script": null,
      "tests": "10/10",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "image-metadata-editor",
      "name": "Image Metadata Editor",
      "category": "other",
      "one_liner": "[mt6] First draft returned 2026-07-10; audited by irp-o.",
      "replaces": "Bridge metadata panel",
      "primitive": "EXIF/IPTC as viewable, settable, strippable fields",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "image-optimizer",
      "name": "Image Optimizer",
      "category": "image",
      "one_liner": "Apply one explicit local recipe to a bounded batch, review selected source and re-encoded output pixels and bytes, verify realized MIME and dimensions, and export an itemized receipt. Best-codec automation, target-byte search, perceptual scoring, cross-engine byte identity, HDR, and metadata preservation are outside scope.",
      "replaces": "Opaque image compressors that hide format fallback, metadata behavior, larger outputs, batch exceptions, and visual tradeoffs",
      "primitive": "Bounded local batch recipe with full preflight, before and after review, exact MIME and decode verification, atomic publication, and itemized evidence",
      "status": "done",
      "runner": "imgtool",
      "script": null,
      "tests": "17/17",
      "from": null,
      "to": null,
      "public": false,
      "search_title": "Batch Image Optimizer with Before and After Review | Private",
      "search_description": "Resize and re-encode up to 20 images locally as WebP, JPEG, or PNG. Review exact dimensions and bytes, verify output MIME, and export a batch receipt.",
      "search_intro": "Image optimization is a tradeoff, not a magic score. This workbench applies one explicit recipe to a bounded batch, then keeps the source and re-encoded image side by side with exact dimensions, bytes, and the decision to keep or skip a larger result. The full batch is preflighted before outputs are published, and browser format fallback is rejected instead of being mislabeled.",
      "how_it_works": [
        "Choose up to 20 images within the displayed byte and pixel limits, then set output format, quality, maximum dimensions, and JPEG background.",
        "Preflight the complete batch, re-encode locally, verify the actual output MIME and decoded dimensions, and hold every download if a file fails.",
        "Review source and output pixels and byte changes, then export verified images with a versioned JSON receipt."
      ],
      "use_cases": [
        "Prepare a group of website images with one reproducible size and format recipe.",
        "Inspect whether a WebP or JPEG quality setting produces acceptable visible artifacts before launch.",
        "Record which files became larger, failed, or were intentionally skipped instead of hiding batch exceptions."
      ],
      "limits": [
        "Quality values are encoder inputs, not comparable visual-quality scores across formats or browsers.",
        "Every kept output is re-encoded; EXIF and ancillary metadata are excluded, while color conversion is managed by the browser.",
        "The browser workbench is capped at 20 files, 50 MB input, 32 megapixels per file, 120 megapixels per batch, and 100 MB output."
      ],
      "faq": [
        {
          "q": "Are images uploaded?",
          "a": "No. Decode, resize, encode, review, ZIP packaging, and receipt generation happen in this browser tab."
        },
        {
          "q": "Why can an optimized file be larger?",
          "a": "Format, image content, dimensions, and metadata policy all affect byte size. You can keep larger re-encoded files or record them as explicit skips."
        },
        {
          "q": "Does the tool preserve metadata?",
          "a": "No. Kept browser outputs are re-encoded and exclude EXIF and ancillary metadata. Use the local CLI when you need its explicit ICC-preservation behavior."
        }
      ],
      "related": [
        "image-resizer",
        "favicon-generator",
        "image-diff"
      ]
    },
    {
      "slug": "image-resizer",
      "name": "Image resizer",
      "category": "image",
      "one_liner": "Resize images under an explicit fit rule (contain/cover/pad/stretch) with exact before/after geometry reported.",
      "replaces": "Express one-click resize",
      "primitive": "Explicit visible fit rules (pad/crop/contain) instead of a magic auto-resize",
      "status": "done",
      "runner": "imgtool",
      "script": null,
      "tests": "17/17",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "image_noise_watermark",
      "name": "Image-domain watermark",
      "category": "other",
      "one_liner": "[mt4] DCT-based invisible watermark for image-heavy PDFs. First draft 2026-07-10; audited.",
      "replaces": "Adobe/Acrobat",
      "primitive": "Survives some recompression; paired detector.",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "invisible_dot_fingerprint",
      "name": "Invisible dot fingerprint",
      "category": "other",
      "one_liner": "[mt4] Sparse dot pattern watermark with paired detector. First draft 2026-07-10; audited.",
      "replaces": "Adobe/Acrobat",
      "primitive": "Seeded, reproducible, with extractor.",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "leak_attribution_suite",
      "name": "Leak attribution suite",
      "category": "other",
      "one_liner": "[mt4] Canary tokens, integrity seal, redaction verify, expiring links. First draft 2026-07-10; audited.",
      "replaces": "Adobe/Acrobat",
      "primitive": "Combined secure-delivery toolkit.",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "legal-hr-pdf-cleanup",
      "name": "Legal/HR PDF cleanup tool",
      "category": "other",
      "one_liner": "Chat flagship #5",
      "replaces": "Acrobat compare/redact/split pieces",
      "primitive": "one office worker under time pressure as the unit",
      "status": "planned",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "math-vocab-cards",
      "name": "Math Vocab Cards",
      "category": "other",
      "one_liner": "[batch-draft] CSV of terms -> printable duplex flashcards. Specced in batch-drafts; built in Kimi swarm, awaiting download to disk.",
      "replaces": "Canva flashcard templates",
      "primitive": "the vocab CSV is the unit; duplex-aligned card sheets out",
      "status": "planned",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "microtext_watermark",
      "name": "PDF microtext watermark",
      "category": "other",
      "one_liner": "[mt4] Tiny readable text in margins/gutters; readability check. First draft 2026-07-10; audited.",
      "replaces": "Adobe/Acrobat",
      "primitive": "Refuses unreadably tiny output.",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "mindmap_from_doc",
      "name": "Mind map from document",
      "category": "other",
      "one_liner": "[mt4] First draft returned 2026-07-10; audited by irp-o.",
      "replaces": "Adobe topic-graph",
      "primitive": "hierarchical topic extraction",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "office-to-pdf",
      "name": "Office To PDF",
      "category": "pdf",
      "one_liner": "Convert Word/Excel/PowerPoint/OpenDocument/text and images to PDF, one output per input.",
      "replaces": "Adobe Acrobat page tooling and the ilovepdf-class web utilities",
      "primitive": "Orchestration around existing render engines (soffice for documents, Pillow for images, cupsfilter fallback for text) instead of reimplementing layout.",
      "status": "done",
      "runner": "cli",
      "script": null,
      "tests": "3/3",
      "from": "office",
      "to": "pdf",
      "public": false
    },
    {
      "slug": "palette-extractor",
      "name": "Palette extractor",
      "category": "image",
      "one_liner": "Extract dominant colors as paste-ready code (CSS vars/SCSS/Tailwind/JSON/hex) with coverage share and WCAG ink per swatch.",
      "replaces": "Express color/palette pickers",
      "primitive": "Palette as code you paste (CSS vars/Tailwind/JSON), not a picture of swatches",
      "status": "done",
      "runner": "imgtool",
      "script": null,
      "tests": "15/15",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-accessibility-remediation",
      "name": "PDF accessibility remediation assistant",
      "category": "pdf",
      "one_liner": "Chat flagship #3",
      "replaces": "Acrobat accessibility checker",
      "primitive": "visual, semi-automatic remediation (reading order/tags/alt-text)",
      "status": "planned",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-buyer-stamp",
      "name": "PDF Buyer Stamp",
      "category": "pdf",
      "one_liner": "[mt6] Per-row stamped PDF from a buyers CSV. First draft 2026-07-10; audited.",
      "replaces": "manual per-buyer watermarking",
      "primitive": "the order record (CSV row) as the unit, one stamped PDF per buyer",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-canary-link",
      "name": "PDF Canary Link",
      "category": "pdf",
      "one_liner": "[mt6] Unique per-copy tracking link annotation; --verify. First draft 2026-07-10; audited.",
      "replaces": "watermark-only leak tracing",
      "primitive": "a unique per-copy URL as the traceable primitive",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-certificate",
      "name": "PDF Certificate Generator",
      "category": "pdf",
      "one_liner": "[batch-draft] CSV -> certificates on a reportlab template. Specced in batch-drafts; built in Kimi swarm, awaiting download to disk.",
      "replaces": "Canva certificate templates",
      "primitive": "the roster CSV as the unit, batch PDFs out",
      "status": "planned",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-dupe-pages",
      "name": "PDF Duplicate Pages",
      "category": "pdf",
      "one_liner": "[batch-draft] Content-hash duplicate pages; keep the first. Specced in batch-drafts; built in Kimi swarm, awaiting download to disk.",
      "replaces": "Eyeballing a merged packet or re-scanned stack for the same page appearing twice.",
      "primitive": "page-as-visual-fingerprint: sameness judged from a rendered thumbnail hash, catching dupes that differ only in metadata/object order.",
      "status": "planned",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-form-fill",
      "name": "PDF Form Fill",
      "category": "pdf",
      "one_liner": "[mt6] Fill AcroForm fields from JSON/CSV; refuses XFA. First draft 2026-07-10; audited.",
      "replaces": "Acrobat Fill and Sign",
      "primitive": "AcroForm fields filled from data rows; personal signature stamp",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-hash-manifest",
      "name": "PDF Hash Manifest",
      "category": "pdf",
      "one_liner": "[mt6] SHA-256 file + per-page hashes as CSV/JSON. First draft 2026-07-10; audited.",
      "replaces": "ad-hoc checksum scripts",
      "primitive": "per-file and per-page SHA-256 as a manifest artifact",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-insert-pages",
      "name": "PDF Insert Pages",
      "category": "pdf",
      "one_liner": "Insert another PDF's pages (all or a spec) at a 1-based position or 'end'.",
      "replaces": "Adobe Acrobat page tooling and the ilovepdf-class web utilities",
      "primitive": "Positional merge with sub-document selection in one step, replacing split-then-merge round trips.",
      "status": "done",
      "runner": "cli",
      "script": null,
      "tests": "3/3",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-invoice",
      "name": "PDF Invoice Generator",
      "category": "pdf",
      "one_liner": "[batch-draft] CSV/JSON line items -> branded invoice PDF. Specced in batch-drafts; built in Kimi swarm, awaiting download to disk.",
      "replaces": "Express invoice templates",
      "primitive": "line items in, branded deterministic PDF out",
      "status": "planned",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-js-audit",
      "name": "PDF JavaScript Audit",
      "category": "pdf",
      "one_liner": "[mt6] Find JS + launch/open actions; severity ranked. First draft 2026-07-10; audited.",
      "replaces": "enterprise PDF security suites",
      "primitive": "scripts/actions as first-class listable objects",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-link-audit",
      "name": "PDF Link Audit",
      "category": "pdf",
      "one_liner": "[mt6] List internal/external links; flag broken internal. First draft 2026-07-10; audited.",
      "replaces": "manual link QA",
      "primitive": "every link + destination validity as a report",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-metadata-fingerprint",
      "name": "PDF Metadata Fingerprint",
      "category": "pdf",
      "one_liner": "[mt6] Embed buyer/order values into XMP + info keys; --verify. First draft 2026-07-10; audited.",
      "replaces": "third-party licensing wrappers",
      "primitive": "buyer identity embedded in XMP, verifiable offline",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-metadata-view",
      "name": "PDF Metadata Viewer",
      "category": "pdf",
      "one_liner": "[batch-draft] Dump document info, XMP and hidden fields. Specced in batch-drafts; built in Kimi swarm, awaiting download to disk.",
      "replaces": "Acrobat properties dialog",
      "primitive": "every field incl XMP + hidden, one dump",
      "status": "planned",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-print-safe",
      "name": "PDF Print Safe",
      "category": "pdf",
      "one_liner": "[batch-draft] Overlay trim/bleed/safe-zone guides for proofs. Specced in batch-drafts; built in Kimi swarm, awaiting download to disk.",
      "replaces": "Eyeballing a proof to guess whether content is too close to the trim edge.",
      "primitive": "margin-safety-as-measurement: content checked against a real safe rectangle per page, with optional drawn guides.",
      "status": "planned",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-protect",
      "name": "PDF Protect",
      "category": "pdf",
      "one_liner": "Password-protect (AES-256 default) or strip protection with the current password.",
      "replaces": "Adobe Acrobat page tooling and the ilovepdf-class web utilities",
      "primitive": "Modern AES-256 default with explicit algorithm choice; commodity tools still default to RC4 or hide the cipher.",
      "status": "done",
      "runner": "cli",
      "script": null,
      "tests": "3/3",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-qr-stamp",
      "name": "PDF QR Stamp",
      "category": "pdf",
      "one_liner": "[mt6] Stamp a QR (template per file) on chosen pages. First draft 2026-07-10; audited.",
      "replaces": "mail-merge QR plugins",
      "primitive": "templated unique QR per file/page",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-separator-sheets",
      "name": "PDF Separator Sheets",
      "category": "pdf",
      "one_liner": "[mt6] Separator/routing coversheets with QR from a CSV. First draft 2026-07-10; audited.",
      "replaces": "manual batch-scan prep",
      "primitive": "the packet list (CSV) generates separators + routing covers",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-to-lms",
      "name": "PDF-to-LMS converter",
      "category": "pdf",
      "one_liner": "Chat flagship #1",
      "replaces": "Acrobat PDF->HTML + Express templates",
      "primitive": "composition pipeline: shells out to engine convert --use-ml (PDF->IR w/ docling T2 structure), then segments IR heading blocks into per-MPEP-section records -> structured JSON intermediate. Not a standalone extractor.",
      "status": "planned",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": "pdf",
      "to": "lms",
      "public": false
    },
    {
      "slug": "pdf_to_pdfa",
      "name": "PDF to PDF/A",
      "category": "other",
      "one_liner": "[mt4] Convert to PDF/A with strict validation. First draft 2026-07-10; audited.",
      "replaces": "Adobe/Acrobat",
      "primitive": "Never claims success before validation passes.",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "per_copy_layout_fingerprint",
      "name": "Per-copy layout fingerprint",
      "category": "other",
      "one_liner": "[mt4] Seeded spacing/position perturbations for traitor tracing. First draft 2026-07-10; audited.",
      "replaces": "Adobe/Acrobat",
      "primitive": "Safe-variance grading; warns on forms.",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pixel-canvas",
      "name": "Pixel-art controllable-output tool",
      "category": "other",
      "one_liner": "Controllable sprite output via a mathematical intermediary language (ratios/topology), not pixel guessing.",
      "replaces": "AI pixel-art / screenshot-to-pixel generators",
      "primitive": "ratios/topology/object-relationships, not pixels",
      "status": "done",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "presentation_from_doc",
      "name": "Presentation from doc",
      "category": "other",
      "one_liner": "[mt4] First draft returned 2026-07-10; audited by irp-o.",
      "replaces": "Adobe PPT-from-doc",
      "primitive": "outline + slide generation",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "price-sheet-generator",
      "name": "Price Sheet Generator",
      "category": "other",
      "one_liner": "[mt6] First draft returned 2026-07-10; audited by irp-o.",
      "replaces": "InDesign catalog workflow",
      "primitive": "rows in, sectioned price-sheet PDF out (document compiler)",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pricing-table-builder",
      "name": "Pricing Table Builder",
      "category": "other",
      "one_liner": "[mt6] JSON/CSV tiers into a self-contained HTML pricing table. First draft 2026-07-10; audited.",
      "replaces": "web-builder pricing widgets",
      "primitive": "tier data -> one self-contained HTML artifact",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "qr-generator",
      "name": "QR generator",
      "category": "image",
      "one_liner": "QR codes as deployment artifacts: typed validated payloads, CSV batch runs, and millimeter print sizing with a module-size floor check.",
      "replaces": "Canva/Express one-off QR boxes",
      "primitive": "QR as a deployment artifact (typed payloads, batch, print-safe sizing), not a single image",
      "status": "done",
      "runner": "cli",
      "script": null,
      "tests": "17/17",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "quiz_maker",
      "name": "Quiz maker",
      "category": "other",
      "one_liner": "[mt4] First draft returned 2026-07-10; audited by irp-o.",
      "replaces": "Adobe quiz generation",
      "primitive": "question generation + distractors",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "repair_pdf",
      "name": "PDF repair",
      "category": "other",
      "one_liner": "[mt4] 3-tier repair (qpdf/pikepdf/mutool) + health report. First draft 2026-07-10; audited.",
      "replaces": "Adobe/Acrobat",
      "primitive": "Progressive salvage with a fidelity check.",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "responsive-check",
      "name": "Responsive Check",
      "category": "other",
      "one_liner": "Answer one question a page cannot answer about itself: does it side-scroll at phone widths, and which element causes it. Renders the page inside an iframe of the target width (320/360/390/430 by default), reports scrollWidth vs viewport per width, and names the outermost unclipped element past the right edge. Takes a local HTML path or an http(s) URL. --strict exits non-zero so a layout regression fails a build.",
      "replaces": "Chrome DevTools device mode / BrowserStack-style responsive checking: one width at a time, judged by eye, on one page, with no exit code.",
      "primitive": "the iframe as the viewport, not the browser window: headless Chrome floors its window near 500px and silently ignores a smaller --window-size, so sub-500px probes measure 500px and report a false pass; an iframe of the target width has no floor, so media queries resolve honestly at 320/360/390/430 and the check can run as a CI gate.",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "scan_to_pdf",
      "name": "Scan to PDF",
      "category": "other",
      "one_liner": "Drafted in the Suite micro-tool pack. OCR + scan cleanup -> searchable PDF.",
      "replaces": "",
      "primitive": "",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "schema-markup-generator",
      "name": "Schema Markup Generator",
      "category": "other",
      "one_liner": "[mt6] Page facts into validated JSON-LD blocks. First draft 2026-07-10; audited.",
      "replaces": "SEO plugin black boxes",
      "primitive": "page facts -> validated JSON-LD, refuses incomplete input",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "seller-asset-suite",
      "name": "Shopify/Etsy image-and-document utility suite",
      "category": "other",
      "one_liner": "Chat flagship #4",
      "replaces": "Express generic brand/design work",
      "primitive": "the seller's listing pipeline as the unit",
      "status": "planned",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "sentence-reader",
      "name": "Sentence reader",
      "category": "other",
      "one_liner": "Turn an annotated passage into a self-contained interactive reader: hover to highlight a sentence, tap to see a plain-words restatement and an explanation, toggle the whole passage original/simplified.",
      "replaces": "Acrobat read-aloud and highlight-level PDF annotation (document- or highlight-level markup that cannot explain a specific sentence on demand)",
      "primitive": "The sentence is the addressable unit: each sentence carries its own tap-to-reveal restatement and explanation, over a passage that toggles between original and simplified wording",
      "status": "done",
      "runner": "cli",
      "script": null,
      "tests": "5/5",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "sprite-sheet-builder",
      "name": "Sprite Sheet Builder",
      "category": "other",
      "one_liner": "[mt6] Pack many images into one sheet + JSON/CSS coordinates. First draft 2026-07-10; audited.",
      "replaces": "TexturePacker basics",
      "primitive": "pack + coordinates artifact (JSON/CSS), deterministic",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "structural_fingerprint",
      "name": "Structural fingerprint",
      "category": "other",
      "one_liner": "[mt4] Low-level per-copy object perturbations for tracing. First draft 2026-07-10; audited.",
      "replaces": "Adobe/Acrobat",
      "primitive": "Invisible structural differentiation.",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "svg-a11y-fixer",
      "name": "SVG Accessibility Fixer",
      "category": "svg",
      "one_liner": "[mt6] First draft returned 2026-07-10; audited by irp-o.",
      "replaces": "manual aria patching",
      "primitive": "title/desc/role injection as a batch pass",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "svg-clean",
      "name": "SVG Clean",
      "category": "svg",
      "one_liner": "Clean, sanitize, and normalize exported SVG files for safe reuse in production pages, React components, icon systems, and asset pipelines.",
      "replaces": "The cleanup pass normally done manually after exporting SVG from Canva, Illustrator, Figma, or another design editor.",
      "primitive": "Single lxml-based pipeline: strip metadata, remove active content, sanitize URL refs, prefix IDs, normalize responsive sizing - opinionated defaults instead of chained tools.",
      "status": "done",
      "runner": "svgworkbench",
      "script": null,
      "tests": "11/11",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Clean an SVG for the Web | Sanitize, Prefix IDs, Make Responsive",
      "search_description": "One pass that strips editor metadata, removes active content, prefixes ids so several inlined files stop colliding, and adds a viewBox so the graphic scales. With a before and after preview.",
      "search_intro": "An SVG exported from a design tool is rarely ready to inline. It carries provenance metadata nobody needs, it may carry active content, its ids are generic enough to collide with the next file pasted into the same page, and it often has fixed pixel dimensions and no viewBox so it refuses to scale. Those are four separate chores usually done by four separate tools or by hand. This does all four in one pass and shows the drawing before and after, because the risk of a combined clean is that one of the four steps quietly changes how the file looks.",
      "how_it_works": [
        "Choose one or more SVG files.",
        "Set the id prefix, or turn id prefixing off if the file is used alone.",
        "Leave responsive sizing on to get a viewBox and fluid width, or turn it off to keep the original dimensions.",
        "Check the before and after drawings, read what changed, and download."
      ],
      "use_cases": [
        "Preparing an icon set for inlining into one page without id collisions between icons.",
        "Getting a designer's export ready to commit to a repository.",
        "Making a fixed-size logo scale inside a responsive header."
      ],
      "limits": [
        "Id prefixing rewrites references it can see in attributes and style text; a reference built at runtime in JavaScript is not rewritten.",
        "Responsive sizing needs numeric width and height to derive a viewBox; if they are percentages or missing, it says so and leaves the file alone.",
        "This does not reduce path precision or merge shapes. Minify is the tool for file size."
      ],
      "faq": [
        {
          "q": "What is id prefixing for?",
          "a": "Two inlined SVGs that both define a gradient called a will collide, and the second one wins for both. Prefixing makes the ids unique per file and rewrites the references to match."
        },
        {
          "q": "Does this sanitize as well?",
          "a": "Yes, it runs the same safety pass as the sanitize tool, including the animation-target check."
        },
        {
          "q": "Why did my file get bigger?",
          "a": "Prefixed ids are longer and responsive sizing adds attributes. Run minify afterwards if size matters more than either."
        }
      ],
      "related": [
        "svg-sanitize",
        "svg-minify",
        "svg-id-prefix"
      ]
    },
    {
      "slug": "svg-css-background",
      "name": "SVG CSS Background",
      "category": "svg",
      "one_liner": "Emit CSS background-image rules that inline each SVG as a percent-encoded data URL with sensible default background sizing.",
      "replaces": "The manual chore of base64-or-URL-encoding an SVG and pasting it into a CSS rule with the right background-* properties (or running a separate encoder tool and writing the CSS by hand).",
      "primitive": "Deterministic percent-encoded data URL (via urllib.parse.quote with safe=\"\") plus an opinionated fixed rule body (no-repeat, center, contain) and a slugified selector derived from the input filename.",
      "status": "done",
      "runner": "svgworkbench",
      "script": null,
      "tests": "6/6",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "SVG to CSS Background | Encoded data URI, Ready to Paste",
      "search_description": "Turn an SVG into a CSS background-image rule as an encoded data URI, with an optional selector, so a vector can be used from a stylesheet without a second network request.",
      "search_intro": "Using an SVG as a CSS background means embedding it in the stylesheet as a data URI, and doing that by hand fails on the characters CSS treats specially. The usual result is a rule that silently paints nothing, which is hard to debug because the stylesheet is valid and the selector matches. This produces the encoded rule directly from the file, with the selector filled in if one is given, and leaves the result editable so it can be adjusted before it is pasted.",
      "how_it_works": [
        "Choose an SVG file.",
        "Optionally give the CSS selector the rule should use.",
        "Run it and read the generated rule beside the original drawing.",
        "Edit it if needed, then copy or download the CSS."
      ],
      "use_cases": [
        "Embedding an icon in a stylesheet without an extra request for it.",
        "Producing a background rule for a custom checkbox, bullet or chevron.",
        "Getting a correctly encoded data URI without debugging escaping by hand."
      ],
      "limits": [
        "A large SVG makes a large stylesheet, and unlike a separate file it cannot be cached independently.",
        "CSS cannot restyle a background SVG, so a colour change means regenerating the rule.",
        "Embedded raster images travel inside the data URI and make it very large; extract them first if that matters."
      ],
      "faq": [
        {
          "q": "Why not just link the file?",
          "a": "A separate file is usually better for anything large or reused. A data URI is worth it for small icons where the extra request costs more than the bytes."
        },
        {
          "q": "Can I change the colour in CSS?",
          "a": "No. The colour is inside the encoded document. Recolour the SVG and generate the rule again, or inline the SVG instead."
        },
        {
          "q": "Is the file uploaded to encode it?",
          "a": "No. The encoding happens in this tab."
        }
      ],
      "related": [
        "svg-to-react",
        "svg-minify",
        "svg-clean"
      ]
    },
    {
      "slug": "svg-extract-images",
      "name": "SVG Extract Images",
      "category": "svg",
      "one_liner": "Extract embedded base64 PNG/JPEG/GIF/WebP payloads from SVG files, optionally rewriting the SVG to reference the externalized assets.",
      "replaces": "Manual base64 grep-and-decode and the asset-splitting step of commercial SVG optimizers / design-handoff tools.",
      "primitive": "lxml walk over <image> nodes with a strict data-URI matcher, deterministic image-NNN.<ext> naming, and an optional in-place href rewrite to produce a paired externalized SVG.",
      "status": "done",
      "runner": "svgworkbench",
      "script": null,
      "tests": "6/6",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Extract Embedded Images From an SVG | Contact Sheet, No Upload",
      "search_description": "Pull the raster images embedded in an SVG as data URIs back out as real PNG or JPEG files, shown as a contact sheet with sizes, and optionally rewrite the SVG to reference them.",
      "search_intro": "An SVG that is unexpectedly large is usually not a complicated drawing; it is a photograph pasted inside a vector wrapper and stored as base64 text, which costs about a third more than the original file did. Getting those images back out means finding each data URI and decoding it correctly. This extracts every embedded raster as a real file, shows them as a contact sheet with names and sizes so it is obvious what was hiding in there, and can rewrite the SVG to point at the extracted files instead of carrying them.",
      "how_it_works": [
        "Choose the SVG files.",
        "Turn on the rewrite option if the SVG should reference the extracted files afterwards.",
        "Run it and review the extracted images as a contact sheet.",
        "Download an individual image, or take everything as a zip."
      ],
      "use_cases": [
        "Finding out why an SVG is several megabytes.",
        "Recovering a photograph that only survives inside a vector file.",
        "Splitting a self-contained SVG into markup plus an assets folder for a build."
      ],
      "limits": [
        "Only images stored inside the file as base64 data URIs can be extracted. An image referenced by URL is not in the file.",
        "Shapes and paths are drawing instructions, not files, and are not extractable as images.",
        "The rewrite option changes references to a relative assets path, so the SVG and the extracted folder must then travel together."
      ],
      "faq": [
        {
          "q": "Why does my SVG have no extractable images?",
          "a": "Because it is genuinely vector. Only pasted raster content stored as a data URI comes out as a file."
        },
        {
          "q": "Are the extracted images re-encoded?",
          "a": "No. The stored bytes are decoded and written as they were, so the image is exactly what was embedded."
        },
        {
          "q": "What does the rewrite option do?",
          "a": "It replaces each embedded data URI with a relative reference to the extracted file, which is what makes the SVG small."
        }
      ],
      "related": [
        "svg-inspect",
        "svg-minify",
        "pdf-extract-images"
      ]
    },
    {
      "slug": "svg-id-prefix",
      "name": "SVG ID Prefix",
      "category": "svg",
      "one_liner": "Prefix every SVG id and rewrite matching url(#id), href=\"#id\", and ARIA idref references in one pass.",
      "replaces": "Manual find-and-replace of ids (or an editor's rename-and-relink) when inlining multiple SVGs on a single HTML page and avoiding id collisions.",
      "primitive": "Opinionated deterministic id-namespacing: safe-prefix normalization plus tree-wide rewrite of url(...), href, and aria-labelledby/aria-describedby idref lists in a single pass.",
      "status": "done",
      "runner": "svgworkbench",
      "script": null,
      "tests": "6/6",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Prefix SVG IDs to Stop Collisions When Inlining | Local",
      "search_description": "Rename every id in an SVG with a prefix and rewrite the references that point at them, so several inlined files can share a page without their gradients, masks and clips fighting.",
      "search_intro": "Inlining two SVGs into one page merges their id namespaces, and design tools name things predictably enough that collisions are the normal case rather than the unusual one. The failure is confusing because it does not look like a naming problem: one icon renders with another icon's gradient, or a clip path silently applies to the wrong shape. The fix is mechanical but must be complete, because renaming an id without rewriting every reference to it breaks the file more thoroughly than the collision did. This renames and rewrites together, and reports both counts.",
      "how_it_works": [
        "Choose the SVG files that will share a page.",
        "Set a prefix, usually something derived from the file or component name.",
        "Run it and check that the count of rewritten references looks right for the file.",
        "Compare the drawings, then download."
      ],
      "use_cases": [
        "Preparing an icon set to be inlined into one document or sprite.",
        "Fixing an icon that renders with the wrong gradient once a second icon is added to the page.",
        "Namespacing an SVG before it becomes a component used many times."
      ],
      "limits": [
        "References built at runtime by script are not rewritten, because they do not exist in the file.",
        "References from an external stylesheet to an id inside the file will break, since the id has changed.",
        "The prefix is normalised to letters, digits, hyphens and underscores so the result stays a valid id."
      ],
      "faq": [
        {
          "q": "How do I know the rewrite was complete?",
          "a": "The result reports ids prefixed and references rewritten as separate counts, and the before and after drawings are shown side by side, which is where an incomplete rewrite shows up immediately."
        },
        {
          "q": "Does svg-clean already do this?",
          "a": "Yes, as one of its four passes. This tool is for when prefixing is the only thing wanted."
        },
        {
          "q": "What prefix should I use?",
          "a": "Anything unique per file. The component or file name is the usual choice."
        }
      ],
      "related": [
        "svg-clean",
        "svg-to-react",
        "svg-minify"
      ]
    },
    {
      "slug": "svg-inspect",
      "name": "SVG Inspect",
      "category": "svg",
      "one_liner": "Report SVG element counts, ID uniqueness, embedded raster payloads, and structural risk warnings.",
      "replaces": "The audit/reporting surface of SVGO stats, Inkscape's document properties dialog, and ad-hoc XML/lxml scripts teams write to triage exported SVGs.",
      "primitive": "lxml over a hardened parser (entities off, network off) with a fixed set of opinionated risk heuristics (no text nodes, raster-heavy, duplicate ids) surfaced through a stable JSON schema.",
      "status": "done",
      "runner": "svgworkbench",
      "script": null,
      "tests": "6/6",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Inspect an SVG | What Is Actually Inside the File",
      "search_description": "Report what an SVG contains: element counts, embedded raster images and their weight, duplicate ids, dimensions and viewBox, plus warnings about what will not extract as text.",
      "search_intro": "Before changing a vector file it helps to know what kind of file it is, and the answer is not visible from the drawing. A graphic that looks like line art may be a photograph in a wrapper; a file with no text elements will return nothing from a text extractor no matter how many words are visible in the picture; duplicate ids explain rendering that seems random. This reports those facts directly, beside the rendered drawing, so the file's structure and its appearance can be compared in one place.",
      "how_it_works": [
        "Choose one or more SVG files.",
        "Run it and read the counts beside the rendered drawing.",
        "Check the warnings, which name the conditions that break downstream tools.",
        "Download the report as JSON if it is feeding something else."
      ],
      "use_cases": [
        "Finding out why an SVG is far larger than its drawing suggests.",
        "Checking whether visible labels are real text or just shapes before trying to extract them.",
        "Diagnosing an icon that renders inconsistently when duplicate ids are the cause."
      ],
      "limits": [
        "This reports structure, not appearance. It cannot tell you whether the drawing is correct.",
        "Visible words drawn as paths are not text and are counted as paths; recognising them needs OCR.",
        "Element counts are of the document as written, so content hidden by clipping or opacity is still counted."
      ],
      "faq": [
        {
          "q": "Why does it say there are no text nodes when I can read words?",
          "a": "Because those words were converted to outlines when the file was exported. They are shapes now, and only an OCR pass can read them back."
        },
        {
          "q": "What makes duplicate ids a problem?",
          "a": "References resolve to one of them, so gradients, masks and clips can apply to the wrong element. The id prefix tool is the fix."
        },
        {
          "q": "Can I use the report in a script?",
          "a": "Yes, the JSON report is downloadable and is the same structure the command line tool emits."
        }
      ],
      "related": [
        "svg-extract-images",
        "svg-id-prefix",
        "svg-clean"
      ]
    },
    {
      "slug": "svg-minify",
      "name": "SVG Minify",
      "category": "svg",
      "one_liner": "Conservatively minify SVG files without changing geometry: strip comments, optionally strip metadata, convert legacy xlink:href, remove whitespace-only text nodes, and write compact XML.",
      "replaces": "SVGO / online SVG optimizers when the user wants a deterministic, geometry-preserving shrink rather than an opinionated path rewriter.",
      "primitive": "One lxml parse, in-place tree pruning of comments/metadata/blank text, compact tostring - no path decimation, no attribute mangling.",
      "status": "done",
      "runner": "svgworkbench",
      "script": null,
      "tests": "6/6",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Minify SVG Without Guessing | See the Result Before Download",
      "search_description": "Strip comments, metadata and blank text from an SVG and watch the drawing stay identical. Shows the byte count before and after and the result as a percentage of the original.",
      "search_intro": "Minifying a vector file is only useful if the picture survives, and the usual way to find out is to download the result and open it. That loop is slow enough that people either skip minifying or skip checking. This shows the original and the minified file side by side with the new size as a percentage of the old one, so the trade is visible in the same screen where it is made. The passes here are the conservative ones: content that never affected the rendering is removed, and geometry is left alone.",
      "how_it_works": [
        "Choose one or more SVG files.",
        "Optionally keep comments or metadata if something downstream reads them.",
        "Run it and compare the two drawings and the two sizes.",
        "Download the smaller file, or the whole batch as a zip."
      ],
      "use_cases": [
        "Trimming an icon set before it ships in a bundle.",
        "Removing an editor's provenance block from an export without touching the artwork.",
        "Checking how much of a file's weight is actually drawing instructions."
      ],
      "limits": [
        "Path coordinates are not rounded and shapes are not merged, so the reductions here are smaller than tools that alter geometry, and correspondingly safer.",
        "A file that is mostly an embedded raster image will barely shrink; the image is the weight, not the markup.",
        "Comments and metadata are removed by default, which is a problem if a build step reads them."
      ],
      "faq": [
        {
          "q": "Will minifying change how it looks?",
          "a": "It should not, and the point of the side-by-side preview is that you do not have to take that on trust."
        },
        {
          "q": "Why is the saving smaller than other tools report?",
          "a": "This does not round path coordinates. Coordinate rounding gives the largest headline numbers and is also what makes shapes drift."
        },
        {
          "q": "Can I minify a whole folder?",
          "a": "Drop as many files as you like and download them as a zip."
        }
      ],
      "related": [
        "svg-clean",
        "svg-remove-metadata",
        "svg-extract-images"
      ]
    },
    {
      "slug": "svg-ocr-labels",
      "name": "SVG OCR Labels",
      "category": "svg",
      "one_liner": "Rasterize an SVG and OCR the preview with Tesseract to recover visible labels not stored as <text> nodes.",
      "replaces": "The label-recovery slice of commercial CAD/floor-plan OCR tools and 'SVG to searchable' pipelines that bundle their own OCR.",
      "primitive": "PyMuPDF for high-scale rasterization piped into system Tesseract in TSV mode, with confidence + bbox parsed into a stable JSON schema.",
      "status": "done",
      "runner": "cli",
      "script": null,
      "tests": "3/3",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "svg-optimizer",
      "name": "SVG optimizer",
      "category": "svg",
      "one_liner": "[kyle-autobuild b1 #9] opportunity_map: vector/SVG optimizer (easy)",
      "replaces": "Bloated editor-exported SVGs",
      "primitive": "A lossless-by-default minifier with a visible list of what it removed",
      "status": "planned",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "svg-recolor",
      "name": "SVG Recolor",
      "category": "svg",
      "one_liner": "Recolor SVG artwork from an explicit old-to-new color map applied to fills, strokes, gradient stops, inline styles, and <style> rules.",
      "replaces": "The 'recolor artwork' / swatch replacement feature of a vector editor (Illustrator, Inkscape), narrowed to a scriptable, versionable, reproducible palette remap.",
      "primitive": "Deterministic palette map (JSON or --map pairs) plus optional RGB-distance tolerance, with hex-shaped CSS selectors protected by parsing only inside {...} declaration bodies.",
      "status": "done",
      "runner": "svgworkbench",
      "script": null,
      "tests": "19/19",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Recolor an SVG by Exact Colour | Read the Palette First",
      "search_description": "Read the colours actually present in an SVG, then map specific ones to new values with an optional tolerance. Click a swatch to start a mapping line instead of typing hex codes blind.",
      "search_intro": "Recolouring a vector file usually means finding out what colours it uses, which is not something the file volunteers. Opening it in an editor to sample them is slower than the recolour itself, and typing hex codes from memory produces mappings that silently match nothing. This reads the palette out of the file first and shows it as clickable swatches with the number of times each colour appears, so a mapping is built from what is actually there. Tolerance handles files where the same visual colour was exported at several near-identical values.",
      "how_it_works": [
        "Choose one or more SVG files.",
        "Press read the colours to list every flat colour in the file with its usage count.",
        "Click a swatch to start a mapping line, then set the new value after the equals sign.",
        "Raise the tolerance if near-identical shades should be treated as one colour, then run and compare."
      ],
      "use_cases": [
        "Rebranding an icon set from one palette to another.",
        "Making a light-theme illustration work on a dark background.",
        "Collapsing several near-identical exported shades into one brand colour."
      ],
      "limits": [
        "Flat colours are matched. Gradients, patterns and colours inside embedded raster images are not remapped.",
        "A colour written in a form the reader does not recognise will not appear as a swatch and cannot be mapped.",
        "Tolerance matches by colour distance, so a high value can catch shades that were meant to stay different."
      ],
      "faq": [
        {
          "q": "Why did my mapping change nothing?",
          "a": "Almost always because the source colour is not in the file in that exact form. Read the palette first and click the swatch rather than typing the value."
        },
        {
          "q": "What does tolerance do?",
          "a": "It widens each match so colours close to the one named are also replaced, which is what you want when an export produced several shades that were meant to be identical."
        },
        {
          "q": "Can it recolour gradients?",
          "a": "Not the gradient definitions. The colours listed are the flat ones it can match reliably."
        }
      ],
      "related": [
        "svg-clean",
        "svg-inspect",
        "palette-extractor"
      ]
    },
    {
      "slug": "svg-remove-metadata",
      "name": "SVG Remove Metadata",
      "category": "svg",
      "one_liner": "Strip <metadata> elements and Canva/C2PA/XMP/Adobe provenance comments from SVG files while leaving all rendered content intact.",
      "replaces": "Manual editor scrub of Canva/Illustrator/Figma export metadata blocks and provenance comments before publishing.",
      "primitive": "Narrow lxml pass over the shared svg_core.remove_metadata: element and comment sweep only, no attribute or URL rewriting.",
      "status": "done",
      "runner": "svgworkbench",
      "script": null,
      "tests": "6/6",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Strip Metadata and Provenance From an SVG | Nothing Uploaded",
      "search_description": "Remove the metadata block and editor provenance comments a design tool leaves in an SVG export, without touching the artwork or the active-content checks.",
      "search_intro": "Design tools write more than a drawing. An export commonly carries a metadata element and comments identifying the tool, the account, and in some cases a content-provenance record, none of which affect the rendering and all of which ship to anyone who opens the file. Removing them by hand means editing markup and hoping nothing that mattered went with it. This removes the metadata element and the provenance comments specifically, leaves everything else alone, and shows the drawing before and after so it is clear nothing visual moved.",
      "how_it_works": [
        "Choose the exported SVG files.",
        "Run it; the metadata element and known provenance comments are removed.",
        "Check the counts and confirm the drawing is unchanged.",
        "Download the stripped file or the batch as a zip."
      ],
      "use_cases": [
        "Removing an editor's identifying block before publishing a graphic.",
        "Clearing content-provenance comments from an asset going into a repository.",
        "Cutting dead weight out of an export without running a full clean."
      ],
      "limits": [
        "This removes metadata, not active content. Use sanitize for safety, or clean for both.",
        "Provenance comments are matched against known editor markers, so an unusual tool's comment may not be recognised.",
        "Metadata embedded inside an embedded raster image is inside that image, not in the SVG, and is not reached here."
      ],
      "faq": [
        {
          "q": "Does this make the file safe to inline?",
          "a": "No. It removes metadata only. Sanitize is the tool that removes active content, and clean does both."
        },
        {
          "q": "Will it remove my own comments?",
          "a": "Only comments carrying known editor and provenance markers. Ordinary comments stay unless you also minify."
        },
        {
          "q": "Is anything sent anywhere?",
          "a": "No. The file is read and rewritten in this tab."
        }
      ],
      "related": [
        "svg-clean",
        "svg-sanitize",
        "svg-minify"
      ]
    },
    {
      "slug": "svg-render",
      "name": "SVG Render",
      "category": "svg",
      "one_liner": "Render SVG files to PNG and/or PDF via PyMuPDF.",
      "replaces": "The rasterization slice of Inkscape / CairoSVG / commercial SVG-to-PNG/PDF converters.",
      "primitive": "PyMuPDF (fitz) as the SVG-to-raster/PDF engine, driven by opinionated defaults (scale=2.0, format=png) and a batch directory pipeline.",
      "status": "done",
      "runner": "cli",
      "script": null,
      "tests": "3/3",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "svg-responsive",
      "name": "SVG Responsive",
      "category": "svg",
      "one_liner": "Add a missing viewBox from numeric width/height and set root sizing (width=100%, height=auto, preserveAspectRatio, block display) for responsive layouts.",
      "replaces": "The manual editor step of opening each exported SVG to add a viewBox and CSS-friendly root sizing so it scales to its container.",
      "primitive": "Opinionated single-pass root normalization: viewBox synthesized from numeric root width/height, plus a fixed responsive style set merged into the root element.",
      "status": "done",
      "runner": "svgworkbench",
      "script": null,
      "tests": "6/6",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Make an SVG Scale | Add a viewBox and Fluid Sizing",
      "search_description": "Give a fixed-size SVG a viewBox derived from its own dimensions, plus fluid width and a sensible aspect ratio, so it fills its container instead of sitting at one pixel size.",
      "search_intro": "An SVG with width and height in pixels and no viewBox is a fixed-size graphic wearing a vector format. It will not scale with its container, and the usual symptom is a logo that stays small in a wide header or overflows on a phone. The fix is a viewBox describing the coordinate space, plus sizing that lets the element take its container's width. This derives the viewBox from the file's own declared dimensions, so the proportions are the ones the file was drawn at, and reports plainly when the dimensions are not numeric enough to derive anything.",
      "how_it_works": [
        "Choose the SVG files that are refusing to scale.",
        "Run it; a viewBox is added when width and height are numeric.",
        "Check the result, which now sizes to its container in the preview.",
        "Download, or read the warning if the file could not be measured."
      ],
      "use_cases": [
        "Fixing a logo that stays one size no matter how wide its header gets.",
        "Preparing an illustration to sit inside a fluid grid.",
        "Making an icon inherit its size from CSS rather than from the file."
      ],
      "limits": [
        "A file whose width and height are percentages or missing cannot have a viewBox derived; the tool says so rather than inventing one.",
        "A file that already has a viewBox keeps it. Only the sizing attributes and style are adjusted.",
        "Nested SVG elements inside the document are not adjusted, only the root."
      ],
      "faq": [
        {
          "q": "What does a viewBox actually do?",
          "a": "It declares the coordinate space the drawing was made in, which is what lets the renderer map that drawing onto whatever size the element ends up being."
        },
        {
          "q": "Why did it refuse my file?",
          "a": "Because its width and height were not plain numbers, so there was nothing to derive a coordinate space from. Setting a viewBox by guessing would crop or stretch the artwork."
        },
        {
          "q": "Does this change the artwork?",
          "a": "No. It changes the root element's sizing attributes and adds a viewBox; the shapes are untouched."
        }
      ],
      "related": [
        "svg-clean",
        "svg-minify",
        "svg-inspect"
      ]
    },
    {
      "slug": "svg-sanitize",
      "name": "SVG Sanitize",
      "category": "svg",
      "one_liner": "Remove active SVG content, event handlers, remote links, javascript: URLs, CSS imports, and unsafe CSS url(...) references while preserving internal fragment refs and data-image references.",
      "replaces": "Ad-hoc DOMPurify/regex passes people write to make third-party SVGs safe to inline in web pages.",
      "primitive": "Single svg_core.sanitize_tree walk with an allowlist for internal (#id) and data:image references and a denylist for on* attributes, script/foreignObject/iframe/etc., CSS expression, @import, and -moz-binding.",
      "status": "done",
      "runner": "svgworkbench",
      "script": null,
      "tests": "18/18",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Sanitize an SVG Before You Inline It | Runs in the Tab",
      "search_description": "Remove active content from an SVG, including the animation elements that rewrite a link to a javascript URL at runtime. See the picture before and after, and a receipt naming every item removed.",
      "search_intro": "An SVG is a document, not an image, so accepting one is closer to accepting HTML than to accepting a PNG. Most sanitizers decide safety from a list of forbidden element names, which is why a run of recent advisories all describe the same escape: a SMIL animation element is not on the list, and it can set a link's href to a javascript URL after the file has already been checked. This tool decides from what an attribute is allowed to become rather than from what an element is called, shows the drawing before and after so a destructive clean is visible immediately, and lists every removed item by name.",
      "how_it_works": [
        "Choose one SVG or a batch. Nothing is uploaded.",
        "The file is parsed, checked and rewritten in this tab.",
        "Compare the original and the result side by side; the preview loads each as an image, which cannot run script.",
        "Read the receipt to see exactly what came out, then download the sanitized file."
      ],
      "use_cases": [
        "Vetting an SVG logo or icon from a marketplace before inlining it in a page.",
        "Clearing user-uploaded vector files that will be served back to other people.",
        "Checking whether a file a scanner flagged actually carries active content."
      ],
      "limits": [
        "An animation whose target is a link is removed outright rather than repaired, so a file that animated a link loses that animation.",
        "This checks the document, not the rendering. A file that is safe can still draw something misleading.",
        "External references are removed rather than fetched and inspected, so a legitimate remote reference will not survive."
      ],
      "faq": [
        {
          "q": "Is removing script tags not enough?",
          "a": "No. An animation element can install a value on another element after parsing, so a check that only reads element names never sees the value it will install. Published advisories against several products describe exactly that bypass."
        },
        {
          "q": "Will my animations survive?",
          "a": "Animations of paint and geometry such as fill, offset, d, width and transform are kept. Only animations that target a link or source attribute, or that carry a URL value the tool refuses, are removed."
        },
        {
          "q": "Is it safe to preview a hostile file here?",
          "a": "Yes. Both previews load the markup as an image rather than inserting it into this page, and an SVG loaded as an image cannot run script."
        }
      ],
      "related": [
        "svg-clean",
        "svg-remove-metadata",
        "svg-inspect"
      ]
    },
    {
      "slug": "svg-sprite-builder",
      "name": "SVG Sprite Builder",
      "category": "svg",
      "one_liner": "[mt6] First draft returned 2026-07-10; audited by irp-o.",
      "replaces": "Illustrator export + manual sprite assembly",
      "primitive": "many icons -> one symbol sheet + use refs",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "svg-stroke-to-path",
      "name": "SVG stroke to path",
      "category": "svg",
      "one_liner": "[mt6] First draft returned 2026-07-10; audited by irp-o.",
      "replaces": "Illustrator outline-stroke",
      "primitive": "Geometric stroke outlining to filled shapes for cross-engine consistency",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": "svg-stroke",
      "to": "path",
      "public": false
    },
    {
      "slug": "svg-to-css",
      "name": "SVG to CSS",
      "category": "svg",
      "one_liner": "[kyle-autobuild b1 #8] opportunity_map: vector/Vector-to-CSS/HTML (easy)",
      "replaces": "Manual SVG-to-markup work",
      "primitive": "An SVG as front-end code you ship, not a design file you re-export",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": "svg",
      "to": "css",
      "public": false
    },
    {
      "slug": "svg-to-jpg-webp",
      "name": "SVG to JPG/WebP",
      "category": "svg",
      "one_liner": "[mt6] First draft returned 2026-07-10; audited by irp-o.",
      "replaces": "Editor/online SVG raster export",
      "primitive": "Background-aware raster export at chosen sizes and formats; extends the PNG/PDF renderer",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": "svg",
      "to": "jpg-webp",
      "public": false
    },
    {
      "slug": "svg-to-react",
      "name": "SVG to React",
      "category": "svg",
      "one_liner": "Convert SVG files into React JSX component source with hyphenated attributes rewritten to camelCase, class to className, inline style parsed to a JSX object, and {...props} spread on the root.",
      "replaces": "Hand-porting an SVG into a React component (or reaching for a full transformer toolchain like SVGR) when a small, opinionated JSX conversion is all that is needed.",
      "primitive": "Deterministic AST walk over the parsed SVG with a fixed attribute rename table (class -> className, aria-*/data-* preserved, kebab -> camel elsewhere), style-string to JSX-object conversion, and {...props} on the root component.",
      "status": "done",
      "runner": "svgworkbench",
      "script": null,
      "tests": "6/6",
      "from": "svg",
      "to": "react",
      "public": true,
      "search_title": "SVG to React Component | JSX Attributes, Editable Output",
      "search_description": "Convert an SVG into a React component with attributes renamed to their JSX spelling, a named export, and the result editable before you copy it into a project.",
      "search_intro": "Pasting raw SVG markup into JSX does not work, because the attribute names differ: stroke-width, clip-path, fill-rule and their neighbours all need the camelCase spelling, and the compiler's error points at the element rather than the attribute. Converting by hand is a rename pass that is easy to do incompletely. This produces a component with the attributes translated and a sensible component name, and leaves the output editable so props, sizing or a className can be added before it goes into a file.",
      "how_it_works": [
        "Choose an SVG file.",
        "Optionally set the component name; otherwise one is derived from the file name.",
        "Run it and read the component beside the original drawing.",
        "Edit the JSX if needed, then copy or download it."
      ],
      "use_cases": [
        "Turning a designer's icon export into a component without hand-renaming attributes.",
        "Getting a logo into a React app as markup rather than an image request.",
        "Producing a starting point for an icon component that will take size and colour props."
      ],
      "limits": [
        "The output is a plain component. Props for size, colour or title are a manual addition, which is why the result stays editable.",
        "Ids inside the component are not namespaced, so rendering the same component twice on a page can collide; prefix the ids first if it uses gradients or masks.",
        "Style attribute contents are carried across as written and may need converting to an object for some setups."
      ],
      "faq": [
        {
          "q": "Why can I not paste SVG straight into JSX?",
          "a": "Because hyphenated SVG attribute names are not valid JSX property names. They have to be renamed to their camelCase spellings."
        },
        {
          "q": "Will the same icon twice on one page break?",
          "a": "It can, if the file defines ids for gradients, masks or clips. Run the id prefix tool first."
        },
        {
          "q": "Can I add props to it?",
          "a": "Yes. The output is editable here before you copy it."
        }
      ],
      "related": [
        "svg-id-prefix",
        "svg-clean",
        "svg-css-background"
      ]
    },
    {
      "slug": "text_level_fingerprint",
      "name": "Text-level fingerprint",
      "category": "other",
      "one_liner": "[mt4] Context-aware synonym substitution for narrative text. First draft 2026-07-10; audited.",
      "replaces": "Adobe/Acrobat",
      "primitive": "Dry-run first; never on legally binding text.",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "ticket-generator",
      "name": "Ticket Generator",
      "category": "other",
      "one_liner": "[mt6] CSV rows into tickets: unique IDs + QR, N-up. First draft 2026-07-10; audited.",
      "replaces": "Canva ticket templates",
      "primitive": "CSV rows -> unique-ID QR tickets, N-up",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "unlock_pdf",
      "name": "PDF unlock",
      "category": "other",
      "one_liner": "[mt4] Remove password/encryption from user-owned PDFs. First draft 2026-07-10; audited.",
      "replaces": "Adobe/Acrobat",
      "primitive": "Preserves internals; owner/open password.",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "video-compress",
      "name": "Video Compress",
      "category": "other",
      "one_liner": "[mt6] First draft returned 2026-07-10; audited by irp-o.",
      "replaces": "Media Encoder presets",
      "primitive": "named honest targets (share/web/archive) over a CRF wrapper",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "video-remove-silence",
      "name": "Video Remove Silence",
      "category": "other",
      "one_liner": "[mt6] silencedetect + concat cuts with a dry-run cut list. First draft 2026-07-10; audited.",
      "replaces": "Premiere manual ripple cuts",
      "primitive": "silencedetect cut list, previewable before writing",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "video-slideshow",
      "name": "Video Slideshow",
      "category": "other",
      "one_liner": "[mt6] First draft returned 2026-07-10; audited by irp-o.",
      "replaces": "Express slideshow maker",
      "primitive": "images + durations CSV as the timeline",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "video-to-gif",
      "name": "Video to GIF",
      "category": "other",
      "one_liner": "[kyle-autobuild b1 #10] opportunity_map: express/video to GIF (easy)",
      "replaces": "Express video-to-GIF / clip actions",
      "primitive": "A wrapper that picks good ffmpeg flags (two-pass palettegen/paletteuse) for small clean GIFs",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": "video",
      "to": "gif",
      "public": false
    },
    {
      "slug": "visual-doc-converter",
      "name": "Visual converter for flattened documents",
      "category": "other",
      "one_liner": "Chat flagship #6",
      "replaces": "Acrobat multi-format PDF export",
      "primitive": "preserve visual alignment while making content usable",
      "status": "planned",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "watermark_pdf",
      "name": "PDF watermark",
      "category": "other",
      "one_liner": "[mt4] Vector text watermark with real opacity + adaptive placement. First draft 2026-07-10; audited.",
      "replaces": "Adobe/Acrobat",
      "primitive": "Occlusion-aware: auto above/below content.",
      "status": "in-progress",
      "runner": "cli",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "watermarker",
      "name": "Watermarker",
      "category": "image",
      "one_liner": "Batch-watermark images with text or a logo sized relative to each image, anchored on a 9-point grid or tiled.",
      "replaces": "Express watermarking",
      "primitive": "A batch protection workflow, not a single-image editor",
      "status": "done",
      "runner": "imgtool",
      "script": null,
      "tests": "15/15",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-to-html",
      "name": "PDF to HTML",
      "category": "convert",
      "one_liner": "A pixel-faithful copy of the page whose text is fitted to it, reviewable run by run before export.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfhtml",
      "op": "html",
      "script": null,
      "tests": "",
      "from": "pdf",
      "to": "html",
      "public": true,
      "search_title": "Convert a PDF to HTML that keeps the design and the real text",
      "search_description": "Turn a PDF into a web page that looks exactly like the PDF and whose words are real HTML, each one fitted to the picture underneath it. Review and retype any run before export. Runs locally, nothing is uploaded.",
      "search_intro": "There are two honest ways to turn a PDF into a web page and they answer different questions. A reflowed conversion throws the layout away and gives back headings and paragraphs that resize with the window, which is what an article wants. A faithful conversion keeps the page exactly as it was drawn and lays the real words over the top of it, which is what a form, a certificate, a statement or a designed layout wants, because in those documents the position of a number is part of its meaning. This page does the second. Every page is drawn once as a picture and every text run from the source is placed over it as a transparent, selectable, copyable HTML element. What usually goes wrong with that approach is that the invisible words drift away from the visible ones, so selecting a line highlights half of the line above it. Two things stop that here: each run is placed from its own baseline minus the ascent of the actual font the PDF embedded, rather than from the bottom of a line box, and each run is stretched horizontally until it is exactly as wide as the source said it was. The fit is shown as a number for every run, so the claim can be checked rather than believed.",
      "how_it_works": [
        "Choose a PDF. It is opened in this tab and the page count is read before anything is converted.",
        "Choose what the page should carry. A PNG picture for a designed page, a JPEG one for a scan, or no picture at all when only the text and its positions matter. Resolution, JPEG quality, font embedding and a page range are all decisions here, and each one is reflected in the byte breakdown of the result.",
        "Convert, then review. The converted page renders beside the list of text runs that produced it. Selecting a run outlines it in the page and shows the size, the position, the width the source reported, the width this font renders, and the fit that was applied.",
        "Retype anything that came through wrong, then download the HTML. A retyped run keeps its position and drops its stretch, because the width the source reported belonged to the words that were replaced."
      ],
      "use_cases": [
        "Put a designed one-page document, a certificate or a statement on the web without losing its layout and without turning it into an image nobody can search or copy from.",
        "Make a PDF's text selectable and copyable in a browser while the page still looks like the original.",
        "Check whether a converter's text layer actually lines up with the page, by reading the fit for each run rather than by dragging the cursor over it."
      ],
      "limits": [
        "The picture is the picture. Images, charts, logos and vector drawings stay inside the page raster and are not extracted as separate files.",
        "Hyperlinks, form fields, annotations and bookmarks are not carried across.",
        "A table stays a visual grid rather than becoming table markup. A spreadsheet is what the CSV and Excel conversions are for.",
        "Rotated and vertical text is positioned but not rotated, so a sideways caption sits horizontally over its own artwork.",
        "Embedding the fonts a PDF used carries the whole font program, not a subset, so a document that uses several faces produces a large file. Font embedding can be switched off, at the cost of the fit falling back to the metrics the source declared.",
        "A scanned page holds no text, so it converts as a picture with nothing to select or retype, and says so."
      ],
      "faq": [
        {
          "q": "Is the file uploaded?",
          "a": "No. It is read, drawn and assembled inside this tab. Nothing leaves the browser."
        },
        {
          "q": "Why is the text invisible in the result?",
          "a": "It is transparent on purpose. The picture already shows the words at full fidelity, so drawing them twice would blur them. The transparent copy is what makes the page selectable, copyable and searchable."
        },
        {
          "q": "What does the percentage next to each run mean?",
          "a": "How far that run had to be stretched horizontally to match the width the source reported for it. A small number means the embedded font renders the run almost exactly as the PDF drew it."
        },
        {
          "q": "Why is the file so large?",
          "a": "A faithful page carries a picture of every page and, by default, the font programs the PDF used. The result names the byte cost of each, so the resolution, the codec and the font decision can each be traded off against it. Text only with no picture is a few kilobytes."
        },
        {
          "q": "When is the reflowing conversion the better answer?",
          "a": "Whenever the document is prose meant to be read on a phone. A faithful page keeps a fixed layout, so it scales but never reflows."
        }
      ],
      "related": [
        "pdf-to-webpage",
        "pdf-to-word",
        "pdf-to-markdown"
      ]
    },
    {
      "slug": "pdf-to-webpage",
      "name": "PDF to web page",
      "category": "convert",
      "one_liner": "A clean, reflowing web page: read the structure, correct it, then build.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfwebpage",
      "op": null,
      "script": null,
      "tests": "",
      "from": "pdf",
      "to": "webpage",
      "public": false
    },
    {
      "slug": "pdf-to-word",
      "name": "PDF to Word",
      "category": "convert",
      "one_liner": "An editable .docx whose headings, tables and paragraphs are real, reviewed before export.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "docstructure",
      "op": "docx",
      "script": null,
      "tests": "",
      "from": "pdf",
      "to": "docx",
      "public": true,
      "search_title": "Convert a PDF to Word with real headings and real tables",
      "search_description": "Get a .docx whose outline fills the navigation pane, whose tables are tables Word can sort, and whose paragraphs keep reflowing as they are typed into. Runs locally, nothing is uploaded.",
      "search_intro": "The reason an converted .docx is painful to edit is rarely the words. It is that each printed line arrived as its own paragraph, so adding a sentence pushes text nowhere and deleting one leaves a ragged hole; that the heading styles were never defined, so the navigation pane is empty and Styles shows nothing to click; and that a table came through as tab-positioned text, so it cannot be sorted, cannot gain a row, and falls apart the moment a column is widened. This page fixes all three before writing the file. It reads the document once, shows what it found as a list of blocks, and lets each wrong guess be corrected on the spot. What is exported is a Word document in the ordinary sense: Heading 1, 2 and 3 are real styles, bullets are a real numbered list, a detected grid is a real table with a header row, and the section is set to the size of paper the source was drawn on.",
      "how_it_works": [
        "Choose a PDF. It is read in this tab, and what was found appears as a list of blocks with the page each came from.",
        "Correct anything read wrong. A block can be retyped as a heading or body text, its wording edited, a wrongly split paragraph merged, a wrongly joined one split, and a stray block dropped.",
        "Download the .docx. It opens in Word with a working navigation pane, editable tables, and paragraphs that reflow as they are typed into."
      ],
      "use_cases": [
        "Pick up editing a report that only exists as a PDF, without first spending an hour deleting line breaks.",
        "Recover a price list or a specification table into a form that can be sorted and given new rows.",
        "Hand a colleague a document they can navigate by heading and mark up with tracked changes."
      ],
      "limits": [
        "Headings are inferred from how much larger the text is than the body. A document that signals its structure only by colour or by indentation needs a few blocks retyped by hand.",
        "Merged cells, nested tables and columns that span are not rebuilt. A grid is recovered when several rows share the same column positions.",
        "Pictures, charts, footnotes and real Word header and footer parts are not carried across. A repeated running head is identified and left out rather than pasted into the body.",
        "A scanned page holds no text to read, so it yields nothing and says so rather than producing an empty file."
      ],
      "faq": [
        {
          "q": "Is the file uploaded?",
          "a": "No. It is read and the Word document is assembled inside this tab. Nothing leaves the browser."
        },
        {
          "q": "Why is the navigation pane empty in files from other converters?",
          "a": "Because they apply a heading style without defining it in the document. Word then has nothing to build an outline from. The file this page writes defines the styles it applies."
        },
        {
          "q": "Will the text still reflow when it is edited?",
          "a": "Yes, provided the wrapped lines were rejoined. That is on by default and can be switched off if the original line breaks matter more."
        },
        {
          "q": "Which paper size does the result use?",
          "a": "The size the source pages were drawn at, so an A4 original stays A4. US Letter or A4 can be forced instead."
        }
      ],
      "related": [
        "pdf-to-epub",
        "pdf-to-markdown",
        "pdf-to-html"
      ]
    },
    {
      "slug": "pdf-to-markdown",
      "name": "PDF to Markdown",
      "category": "convert",
      "one_liner": "Column-aware Markdown with headings, lists and pipe tables, editable before download.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "doctext",
      "op": "md",
      "script": null,
      "tests": "",
      "from": "pdf",
      "to": "md",
      "public": false,
      "search_title": "PDF to Markdown, Column-Aware and Editable | Runs Locally",
      "search_description": "Convert a PDF to Markdown in the browser. Columns are read in order, tables become pipe tables, headings come from text size, and the result stays editable before download.",
      "search_intro": "Most browser converters bucket every text run by its vertical position, which turns a two-column page into sentences spliced across the gutter. This workbench detects the column bands first, reads each band top to bottom, and shows the per-page line and column count it used, so a wrong guess is visible rather than silent. Headings come from the relative text size ladder, detected tables become pipe tables, and the Markdown stays in an editable pane until it is downloaded.",
      "how_it_works": [
        "Choose a PDF up to 60 MB and 200 pages; text extraction and assembly run in this tab.",
        "Pick whether to follow the detected columns or read straight across, set a page range, and choose whether headings are inferred from text size.",
        "Correct anything the detector got wrong in the editable pane, then copy or download; the file carries the edited text, not a regenerated one."
      ],
      "use_cases": [
        "Turn a two-column report or paper into Markdown that reads in the right order.",
        "Prepare document text as Markdown for a notes app, a static site, or a language model prompt.",
        "Pull a table out of a report as a pipe table without retyping it."
      ],
      "limits": [
        "Headings are inferred from relative text size, so a document that styles headings by weight or color alone produces flat Markdown.",
        "Tables are detected from cell alignment; merged cells and multi-row headers are flattened to a single header row.",
        "Image-only pages produce nothing here. The page names them and the OCR workbench handles them first."
      ],
      "faq": [
        {
          "q": "Why does Markdown from other converters mix up my columns?",
          "a": "They group text by vertical position across the whole page width, so the first line of the left column and the first line of the right column land in the same row. This tool detects the vertical gutter no text crosses and reads each column band separately."
        },
        {
          "q": "Can I fix the result before downloading?",
          "a": "Yes. The Markdown pane is editable and the download uses whatever it currently contains. A revert control restores the converted text."
        },
        {
          "q": "Is the PDF uploaded?",
          "a": "No. After the page loads, reading, assembly, editing and download all happen in this browser tab with locally bundled code."
        }
      ],
      "related": [
        "pdf-to-text",
        "pdf-to-webpage",
        "pdf-extract-text"
      ]
    },
    {
      "slug": "pdf-to-text",
      "name": "PDF to text",
      "category": "convert",
      "one_liner": "The words in true reading order, with the column decision visible and correctable.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "doctext",
      "op": "txt",
      "script": null,
      "tests": "",
      "from": "pdf",
      "to": "txt",
      "public": false,
      "search_title": "PDF to Text in Reading Order | Local, Editable, No Upload",
      "search_description": "Extract PDF text in the browser with real reading order. Column bands are detected per page, page-level evidence is shown, and the plain text stays editable before download.",
      "search_intro": "Plain text extraction fails in one specific way that is easy to miss: on a multi-column page the words are all present but the sentences are interleaved, so the file looks fine until it is read. This workbench detects the column bands on each page, reports how many it found, and lets the decision be overridden per conversion. The extracted text of the selected page is shown beside the result so the ordering can be checked against the source rather than trusted.",
      "how_it_works": [
        "Choose a PDF up to 60 MB and 200 pages; nothing leaves the tab.",
        "Follow the detected columns or read straight across, and narrow to a page range such as 1-3,7 when only part of the document matters.",
        "Check the per-page line and column counts, edit the text if needed, then copy or download it."
      ],
      "use_cases": [
        "Get quotable, searchable text out of a two-column paper without the columns interleaving.",
        "Extract a page range from a long report as plain text for a search index or a prompt.",
        "Check whether a PDF actually carries text before sending it through OCR."
      ],
      "limits": [
        "Plain text has no headings, lists or tables; a table becomes aligned words on a line. The Markdown and CSV routes keep that structure.",
        "Reading order is inferred from geometry, so a poster, a form, or a heavily overlapped layout can still come out in an odd order.",
        "Image-only pages produce nothing here and are named page by page."
      ],
      "faq": [
        {
          "q": "Why is text copied from a PDF viewer sometimes scrambled?",
          "a": "A viewer copies in the order the glyphs were drawn, which is not always the order they are read. This tool groups text into column bands and reads each band top to bottom instead."
        },
        {
          "q": "How do I extract only some pages?",
          "a": "Enter a range such as 1-3,7 in the pages field. Pages outside the range stay listed but are marked as not in range."
        },
        {
          "q": "Is the PDF uploaded?",
          "a": "No. Reading, assembly, editing and download happen in this browser tab with locally bundled code."
        }
      ],
      "related": [
        "pdf-to-markdown",
        "pdf-ocr",
        "pdf-extract-text"
      ]
    },
    {
      "slug": "pdf-to-csv",
      "name": "PDF to CSV",
      "category": "convert",
      "one_liner": "A rectangular CSV with the header on row 1 and every excluded line accounted for.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "doctable",
      "op": "csv",
      "script": null,
      "tests": "",
      "from": "pdf",
      "to": "csv",
      "public": false,
      "search_title": "PDF to CSV, Rectangular and Editable | No Upload, No Daily Cap",
      "search_description": "Convert a PDF table to CSV in the browser. Cell alignment defines the columns, every record carries the same field count per RFC 4180, and the grid is editable before export.",
      "search_intro": "A CSV where one line has one field and the next has four is not a CSV a spreadsheet or a script can load, and that is what a converter produces when it treats every visual line as a row. This workbench finds the recurring column alignment on a page, builds a rectangular grid from the rows that match it, and lists every line it kept out along with the reason. The grid is editable, and any excluded line can be pulled in as a row.",
      "how_it_works": [
        "Choose a PDF up to 60 MB and 200 pages; the table is found locally.",
        "Pick a page, decide whether the first detected row is the header, and correct any cell directly in the grid.",
        "Add a column or a row where the detector was too strict, then download a CSV whose records all carry the same field count."
      ],
      "use_cases": [
        "Pull a statement, price list, or results table out of a PDF into a spreadsheet or a script.",
        "Get a loadable CSV out of a report without a daily conversion cap or an upload.",
        "Fix a mis-split column once, in place, instead of re-editing the downloaded file."
      ],
      "limits": [
        "Columns are detected from horizontal alignment, so a table whose cells wrap onto several lines produces one row per line.",
        "Merged cells and multi-row headers are flattened; short rows are padded with empty fields to keep every record the same width.",
        "Scanned tables carry no text and produce no grid. The OCR workbench comes first for those."
      ],
      "faq": [
        {
          "q": "Why do other PDF to CSV tools produce a file my spreadsheet rejects?",
          "a": "They emit one row per visual line, so titles and paragraphs become one-field records mixed in with the real table rows. RFC 4180 expects every record to carry the same field count, which is what this tool enforces."
        },
        {
          "q": "What happens to lines that are not part of the table?",
          "a": "They are listed with the reason they were excluded, and each one can be added to the grid as a row if the detector was wrong."
        },
        {
          "q": "Is the PDF uploaded?",
          "a": "No. Detection, editing and export all happen in this browser tab with locally bundled code."
        }
      ],
      "related": [
        "pdf-to-excel",
        "pdf-to-text",
        "pdf-extract-text"
      ]
    },
    {
      "slug": "pdf-to-excel",
      "name": "PDF to Excel",
      "category": "convert",
      "one_liner": "A .xlsx whose numbers are numbers, built from a grid corrected before export.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "doctable",
      "op": "xlsx",
      "script": null,
      "tests": "",
      "from": "pdf",
      "to": "xlsx",
      "public": false,
      "search_title": "PDF to Excel With Real Numeric Cells | Local, Editable Grid",
      "search_description": "Convert a PDF table to .xlsx in the browser. Numeric cells are written as numbers rather than text, the grid is editable before export, and nothing is uploaded.",
      "search_intro": "A workbook whose every cell is text looks right and behaves wrong: the numbers will not sum, will not sort numerically, and will not chart. This workbench detects the table from cell alignment, shows the inferred type of each column, and writes any cell that reads as a plain decimal as a numeric cell in the .xlsx. Values with leading zeros stay text, because an account number is not a quantity. The grid is editable first, so the workbook matches what was reviewed.",
      "how_it_works": [
        "Choose a PDF up to 60 MB and 200 pages; the table is found locally.",
        "Pick a page and check the per-column type badges, which read number, text, mixed, or empty.",
        "Correct any cell, then download the workbook; cells that read as plain decimals are stored as numbers."
      ],
      "use_cases": [
        "Get a PDF table into Excel or Sheets with figures that sum and sort straight away.",
        "Spot the column where one stray value makes the type mixed before it breaks a formula.",
        "Convert financial or lab tables without uploading them to a conversion service."
      ],
      "limits": [
        "Only plain decimals become numbers. Currency symbols, percent signs, dates and leading-zero identifiers stay text on purpose.",
        "One sheet is written per exported page; merged cells and multi-row headers are flattened.",
        "No formulas, styling, column widths or cell formats are produced; the workbook carries values only."
      ],
      "faq": [
        {
          "q": "Why do converted spreadsheets treat my numbers as text?",
          "a": "Most converters write every cell as an inline string, which Excel reads as text. This tool writes cells that parse as plain decimals as numeric cells instead."
        },
        {
          "q": "Will an account number starting with zero be mangled?",
          "a": "No. A value with a leading zero longer than one character is kept as text, so the zero survives."
        },
        {
          "q": "Is the PDF uploaded?",
          "a": "No. Detection, editing and workbook creation all happen in this browser tab with locally bundled code."
        }
      ],
      "related": [
        "pdf-to-csv",
        "pdf-to-text",
        "pdf-size-analyzer"
      ]
    },
    {
      "slug": "pdf-to-epub",
      "name": "PDF to EPUB",
      "category": "convert",
      "one_liner": "A reflowable e-book whose chapters follow the headings, not the print pages.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "docstructure",
      "op": "epub",
      "script": null,
      "tests": "",
      "from": "pdf",
      "to": "epub",
      "public": true,
      "search_title": "Convert a PDF to EPUB with chapters that follow the headings",
      "search_description": "Make an EPUB 3 that reflows at any type size on a phone or e-reader, with a contents list naming the sections instead of Page 1, Page 2, Page 3. Runs locally, nothing is uploaded.",
      "search_intro": "Reading a PDF on a small screen means pinching and dragging, because its layout is nailed to a sheet of paper that is wider than the screen. An e-book has no sheet: type size is the reader's choice and the words rewrap around it. Getting from one to the other means answering a question the source never recorded, which is where a chapter begins. Most converters answer it with the sheet, one chapter per printed page, and produce a contents list that reads Page 1, Page 2, Page 3 while cutting sections in half wherever the paper happened to run out. This page answers it with the writing instead. It works out the section headings, shows them so a wrong one can be corrected, and lets the level that begins a chapter be chosen. The result is a book whose contents name its sections and whose text rewraps at whatever size it is read at.",
      "how_it_works": [
        "Choose a PDF. Its sections and body text are worked out in this tab and listed for review.",
        "Check the contents panel. Fix any section that was missed or invented, set which heading level begins a chapter, and give the book its title and language.",
        "Download the .epub and open it on a phone, a tablet, an e-reader or in a library app."
      ],
      "use_cases": [
        "Read a long printed manual on a commute at a comfortable type size instead of zooming around a page image.",
        "Add a paper whose only copy is a PDF to an e-reader library, with a working contents list to jump around it.",
        "Produce a first draft e-book to open in a book editor with the sections already in place."
      ],
      "limits": [
        "Sections are inferred from how much larger their text is than the body, so a document that marks them only by colour or position needs a few corrections in the review list.",
        "Illustrations are not carried into the book. Body text, bulleted lists and recovered grids are.",
        "Footnotes, page-number navigation, cover artwork and fixed-layout books are outside what this makes.",
        "A scanned page carries no readable text, so it yields no chapter and is reported rather than silently skipped."
      ],
      "faq": [
        {
          "q": "Is the file uploaded?",
          "a": "No. It is read and the book is assembled inside this tab. Nothing leaves the browser."
        },
        {
          "q": "Which flavour of e-book is written?",
          "a": "EPUB 3, carrying the navigation document that version requires. An older control file is written beside it so an EPUB 2 reader still shows a contents list."
        },
        {
          "q": "Will it open on a Kindle?",
          "a": "Amazon's own Send to Kindle accepts EPUB and converts it on arrival. Kobo, Apple Books, Google Play Books and most library apps open EPUB directly."
        },
        {
          "q": "What decides where one chapter ends and the next starts?",
          "a": "The heading level chosen in the panel. A section that ran across a page break stays in one piece, and the whole document can be kept as a single chapter."
        }
      ],
      "related": [
        "pdf-to-word",
        "pdf-to-markdown",
        "pdf-to-html"
      ]
    },
    {
      "slug": "pdf-to-images",
      "name": "PDF to images",
      "category": "convert",
      "one_liner": "Pick the pages, pick the resolution, get PNG or JPEG files of exactly those pages.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pagerender",
      "op": "png",
      "script": null,
      "tests": "",
      "from": "pdf",
      "to": "png",
      "public": true,
      "search_title": "Turn chosen PDF pages into PNG or JPEG files",
      "search_description": "Pick the pages from a contact sheet, pick the resolution, and get image files of exactly those pages. Runs locally, nothing is uploaded.",
      "search_intro": "Most converters render every page at one fixed resolution and hand back a zip. This page draws a contact sheet first, so the pages are chosen by looking at them rather than by counting in a separate reader, and the resolution is a decision: 72 for the screen, 150 for general use, 300 for print. A page the reader cannot draw is marked in the sheet, left out of the selection, and named, because a picture that is not the page is worse than no picture. Rendering a page and pulling out the pictures already embedded in it are different jobs; this page does the first, and the image extractor does the second.",
      "how_it_works": [
        "Choose a PDF. Every page is drawn small and laid out as a contact sheet in this tab.",
        "Tick the pages to export, or use the all, none, odd, even and range controls.",
        "Set the resolution, the format and the background, then export. One page arrives as a single file and several arrive as a zip, with each page also downloadable on its own."
      ],
      "use_cases": [
        "Lift two diagrams out of a long report at print resolution without exporting the other ninety pages.",
        "Produce screen-resolution page images for a web preview or a thumbnail strip.",
        "Export a design page on a transparent background so it can sit on another layout."
      ],
      "limits": [
        "A page is drawn at the requested resolution only while the result stays inside the 16,777,216 pixel canvas area Safari allows. A larger page is drawn at the highest resolution that fits and the reduction is reported with both numbers.",
        "JPEG has no transparency, so asking for a transparent background with JPEG selected writes white and says so.",
        "Text in the output is pixels, not selectable text. Extracting the words is the text tool's job.",
        "Pulling out the images already embedded in the file is a different tool; this page draws whole pages."
      ],
      "faq": [
        {
          "q": "Is the file uploaded?",
          "a": "No. The PDF is read and drawn in this tab. Nothing leaves the browser."
        },
        {
          "q": "Which resolution should be used?",
          "a": "72 DPI matches a screen, 150 is the usual choice for general use, and 300 is the print standard. Higher numbers make larger files and take longer."
        },
        {
          "q": "Why is one page marked as not drawn?",
          "a": "The reader could not rasterize it, usually a font with no reachable data or a page that took too long. That page is left unselected, and exporting it anyway produces an image with the reason written across the top."
        }
      ],
      "related": [
        "pdf-to-slides",
        "pdf-extract-images",
        "pdf-image-dpi-check"
      ]
    },
    {
      "slug": "pdf-to-slides",
      "name": "PDF to slides",
      "category": "convert",
      "one_liner": "A .pptx whose slides are shaped like the pages, so a portrait document does not arrive letterboxed.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pagerender",
      "op": "pptx",
      "script": null,
      "tests": "",
      "from": "pdf",
      "to": "pptx",
      "public": true,
      "search_title": "Put PDF pages on slides shaped like the pages",
      "search_description": "Build a .pptx whose slide size follows the source page, so a portrait document does not sit letterboxed inside a widescreen slide. Runs locally, nothing is uploaded.",
      "search_intro": "A converter that drops every page into a fixed 4:3 or 16:9 slide leaves a portrait page covering little more than half the slide, with the rest white. This page makes the slide size a decision: shaped like the page, widescreen, or classic, with page-shaped as the default. A filmstrip shows each page already placed inside the slide frame it will occupy, with the percentage of the slide it covers, so the choice is made by looking at the result rather than by opening the deck afterwards.",
      "how_it_works": [
        "Choose a PDF. Every page is drawn small and placed inside the current slide frame.",
        "Pick the slide size and see immediately how much of the slide each page covers.",
        "Tick the pages that should become slides, set the resolution, and build the deck."
      ],
      "use_cases": [
        "Turn a printed handout into a deck without losing half of every slide to white margins.",
        "Move a set of report pages into a presentation for annotation on top of them.",
        "Build a widescreen deck deliberately, when the room's projector needs 16:9 more than the pages need their own shape."
      ],
      "limits": [
        "A slide must be between 1 and 56 inches per side. A page outside that range is clamped and the clamp is reported.",
        "Pages whose shape differs from the first page sit inside the slide with a margin, and the count of those pages is reported.",
        "Each slide is a picture of the page. The text on it is not editable in the presentation; converting to an editable document is the Word tool's job.",
        "Speaker notes are not generated."
      ],
      "faq": [
        {
          "q": "Is the file uploaded?",
          "a": "No. The PDF is read, drawn and packed into the presentation in this tab."
        },
        {
          "q": "Why default to slides shaped like the page?",
          "a": "Most PDFs are portrait, and a portrait page inside a 10 by 7.5 inch slide covers about 58 percent of it. Matching the page shape puts the whole slide to work."
        },
        {
          "q": "Can the slide size be changed afterwards?",
          "a": "Yes, in PowerPoint or Keynote, but the images will then be rescaled to the new shape. Choosing it here avoids that."
        }
      ],
      "related": [
        "pdf-to-images",
        "pdf-to-word",
        "pdf-extract-images"
      ]
    },
    {
      "slug": "html-editor",
      "name": "HTML Editor",
      "category": "code",
      "one_liner": "Open an HTML file, edit it as a rendered page, and save it back as real HTML. The source and the page stay in sync.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "editor",
      "op": null,
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "image-diff",
      "name": "Image Difference Checker",
      "category": "image",
      "one_liner": "Compare exact pixels and tolerated changes with swipe, overlay, heatmap, metrics, and evidence exports.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "imagediff",
      "op": null,
      "script": null,
      "tests": "18/18",
      "from": null,
      "to": null,
      "public": false,
      "search_title": "Compare Images Online | Private Pixel Difference Checker",
      "search_description": "Compare two images privately with swipe, overlay, side-by-side and a pixel heatmap. Measure exact and thresholded changes and export PNG or JSON.",
      "search_intro": "A threshold can tell you that no change is large enough to matter, but it cannot make two different images identical. This comparison workbench keeps those answers separate. It measures exact visible-pixel changes and above-threshold changes, localizes them with an inclusive bounding box, and provides a transparent heatmap and versioned JSON report.",
      "how_it_works": [
        "Choose a reference image and candidate image; strict mode refuses silent dimension changes.",
        "Set the maximum tolerated sRGB channel delta and the background used to compare transparency.",
        "Inspect swipe, overlay, heatmap, or side-by-side views, then download the heatmap and metrics."
      ],
      "use_cases": [
        "Review a before-and-after edit without losing small changes in a visual slider.",
        "Check a visual regression screenshot and locate the region that changed.",
        "Measure whether a re-export or compression pass changed any visible pixels."
      ],
      "limits": [
        "This is deterministic pixel comparison, not perceptual similarity or a quality score.",
        "Images are not automatically aligned, cropped, rotated, or color-profile normalized.",
        "Animated frames, HDR content, and comparisons above 24 million output pixels are not supported."
      ],
      "faq": [
        {
          "q": "Are images uploaded?",
          "a": "No. Decoding, compositing, pixel measurement, preview, and downloads all happen in this browser tab."
        },
        {
          "q": "Why can a result be within threshold but not identical?",
          "a": "Exact identity means every visible channel is equal. Within threshold means differences exist, but none is larger than the tolerance you selected."
        },
        {
          "q": "What happens when dimensions differ?",
          "a": "Strict mode stops and explains the mismatch. You can explicitly choose Resize B to A, after which the report records both original dimensions and the resize."
        }
      ],
      "related": [
        "dupe-finder",
        "image-optimizer",
        "image-resizer"
      ]
    },
    {
      "slug": "pdf-merge",
      "name": "Merge PDFs",
      "category": "pdf",
      "one_liner": "Combine PDFs page by page and keep the bookmarks, fields, and document details.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfop",
      "op": "merge",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Merge PDF Files Locally | Keeps Bookmarks and Form Fields",
      "search_description": "Combine PDFs in this tab with a page board. Reorder or leave out any page, choose how bookmarks are handled, set the document details, and read a receipt of what carried.",
      "search_intro": "Most merge tools quietly throw away the parts of a PDF that are not pages. Bookmarks vanish, form fields stop working, and the title and author of the merged file become whatever library wrote it. This workspace shows every page of every document in the order it will be written, carries the bookmark tree across and remaps it to the pages that survived, relists the form fields so a reader still sees a form, and writes the document details supplied rather than inherited. The same documents with the same settings produce the same bytes every time.",
      "how_it_works": [
        "Add up to 20 PDFs totalling 200 pages. Each one is read locally and reports its page count, bookmarks, form fields, and page sizes before anything is combined.",
        "Work the page board: move documents or individual pages, leave pages out, and turn any page in quarter steps. Every control is a labelled button or select, so no dragging is needed.",
        "Choose whether to keep every bookmark, create one bookmark per document, or remove them, set the title, author, subject, and keywords, then merge and download the PDF with a JSON receipt."
      ],
      "use_cases": [
        "Combine a report, its appendices, and a signed cover sheet while keeping every bookmark pointing at the right page.",
        "Build a packet from selected pages of several documents rather than whole files.",
        "Produce a merged file whose title, author, and dates are set deliberately and reproducibly."
      ],
      "limits": [
        "The browser accepts up to 20 unencrypted PDFs, 50 MiB each, 200 pages in total, and 250 MiB of output. Encrypted or unreadable files are named and refused rather than partly merged.",
        "Digital signatures do not survive any merge. Attachments, page labels, tagged structure, optional content groups, and links that point between the original documents are not carried.",
        "Form fields are relisted from the widgets that landed on the kept pages, and fields sharing a name across documents are renamed so their values do not link. Complex or shared field resources are not reconciled.",
        "Page sizes and rotations are preserved per page. Nothing is scaled to a common size."
      ],
      "faq": [
        {
          "q": "Does merging PDFs lose the bookmarks?",
          "a": "It does in most tools, and it did in this one before this release. The merge now rebuilds the bookmark tree in the output and points every entry at the page it followed. Bookmarks whose page is left out are dropped rather than left pointing somewhere wrong, and the receipt reports how many each document carried."
        },
        {
          "q": "Can I merge only some pages of a document?",
          "a": "Yes. The page board lists every page of every document and any page can be left out or moved. The result is exactly the pages shown, in the order shown."
        },
        {
          "q": "What happens to fillable form fields?",
          "a": "The widgets on the kept pages keep their values and appearances, and the merged file gets a form dictionary again so a reader can see them. Fields with the same name in two documents are renamed so filling one does not fill the other."
        },
        {
          "q": "Will the same files always produce the same merged PDF?",
          "a": "Yes. Creation and modification dates are pinned rather than taken from the clock, so the same documents with the same page plan and settings produce a byte-identical file."
        },
        {
          "q": "Is the PDF uploaded?",
          "a": "No. After this page loads, reading, thumbnails, merging, and downloads happen in this browser tab with locally bundled code."
        }
      ],
      "related": [
        "pdf-split",
        "pdf-reorder",
        "pdf-delete-pages",
        "pdf-extract-pages"
      ]
    },
    {
      "slug": "pdf-split",
      "name": "Split PDF",
      "category": "pdf",
      "one_liner": "Review every output group and page before building private, manifest-verified PDF parts.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfop",
      "op": "split",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false,
      "search_title": "Split PDF by Page or Range | Private Visual Review",
      "search_description": "Split a PDF locally by page, fixed groups, or strict ranges. Review thumbnails, omissions, duplicates, filenames, and a SHA-256 manifest before download.",
      "search_intro": "A split instruction should never turn into a different successful-looking job. This workspace parses the same strict page language as the CLI, previews every output group, blocks malformed or out-of-range terms, and makes omitted or repeated pages an explicit decision. The resulting PDF files and manifest are built locally in a dedicated worker.",
      "how_it_works": [
        "Choose a PDF up to the tested 50 MiB and 60-page browser limits; low-resolution page previews render locally.",
        "Use every page, one file, every N pages, visual split boundaries, or advanced ranges where semicolons start new files and commas stay inside one file.",
        "Review group names, page order, omissions, duplicates, and page-copy caveats, then download one PDF or a manifest-bound ZIP."
      ],
      "use_cases": [
        "Separate a packet, scan, workbook, or exhibit set at visible page boundaries.",
        "Create exact noncontiguous or descending page groups with a deterministic range language.",
        "Produce locally processed parts with page sequences and SHA-256 hashes recorded for handoff."
      ],
      "limits": [
        "Browser input is capped at 50 MiB and 60 pages, and output at 250 MiB; larger jobs should use the local CLI.",
        "Page copying is not a complete PDF catalog clone and can lose or alter bookmarks, forms, attachments, labels, signatures, tags, actions, and other document-level features.",
        "The output is rearranged, not sanitized; page links or actions that survive copying may remain active."
      ],
      "faq": [
        {
          "q": "How do PDF split ranges work?",
          "a": "Commas join pages inside one output and semicolons start another output. For example, 1-3;4-6 makes two three-page PDFs, while 1-3,4-6 makes one six-page PDF. Invalid terms never fall back to another mode."
        },
        {
          "q": "Can a page appear in two split files?",
          "a": "Yes, but cross-output duplication is shown and requires a separate acknowledgement. Omitted pages require their own acknowledgement, and both reset whenever the plan changes."
        },
        {
          "q": "Is the PDF uploaded?",
          "a": "No. After the route loads, PDF reading, thumbnails, page copying, ZIP creation, hashing, and downloads happen in this browser tab with locally bundled runtime code."
        }
      ],
      "related": [
        "pdf-reorder",
        "pdf-extract-pages",
        "pdf-delete-pages"
      ]
    },
    {
      "slug": "pdf-delete-pages",
      "name": "Delete pages",
      "category": "pdf",
      "one_liner": "Remove the pages you tick and keep the bookmarks, fields, and details on the rest.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfop",
      "op": "delete",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Delete Pages from a PDF | Bookmarks and Fields Survive",
      "search_description": "Tick the pages to remove, or type them as 2, 5-7, odd or even. The rest keep their order, their bookmarks follow them, filled form fields keep their values, and nothing is uploaded.",
      "search_intro": "Throwing out a page should cost you that page and nothing else. Most tools rebuild the document from its sheets alone, so the navigation panel empties, anything fillable stops working, and the name in the window bar becomes whichever library did the rebuilding. Nobody is warned, and the damage usually surfaces weeks later when the original is long gone. Here the document survives its own trimming. What you tick leaves; everything else stays in the sequence it was already in. Navigation entries aimed at surviving sheets are renumbered to follow them, entries aimed at discarded sheets are thrown out with them instead of being left aimed at whatever slid into that slot, anything already typed into a surviving sheet is still typed into it, and the name, author, subject and keywords stay exactly as they were. Ask for a sheet the document does not have and you are told so; ask for the lot and you are stopped.",
      "how_it_works": [
        "Pick a document. Before a single sheet moves, this tab reports how long it is, how many navigation entries and fillable boxes it holds, and how much disk it takes.",
        "Tick what should go, or write it out: bare numbers, spans such as 5-7, unbounded spans such as 9-, and the shorthand odd, even and end. Ticks and writing follow one another, and five shortcuts jump to everything, nothing, odd, even, or whatever is not currently ticked.",
        "Read the summary. It counts what the document holds and spells out the fate of each kind of content under the choice currently made, while there is still time to change it.",
        "Confirm, then take away the shortened document and a JSON note recording which original sheets ended up in it and how much of the navigation and how many of the fillable boxes travelled with them."
      ],
      "use_cases": [
        "Throw out the blank sheets a duplex scanner slipped between originals, and still open the result at the chapter you wanted.",
        "Circulate an agreement with the internal costings gone while the countersigned sheet stays fillable and filled.",
        "Prune the wrapper and the trailing matter off a long report without flattening its navigation panel."
      ],
      "limits": [
        "One document at a time, unprotected, at most 50 MiB, leaving at most 200 sheets behind. Protected or damaged files stop the job by name. Its terminal counterpart works at any length.",
        "Naming a sheet the document does not hold stops the job instead of quietly meaning nothing, and asking for all of them stops it too, so the answer is never a document with nothing in it.",
        "Navigation aimed at a discarded sheet goes with it and is never re-aimed. Cross references, attached files, printed sheet labels, accessibility structure and layered content that lean on discarded sheets do not travel.",
        "Cryptographic signing does not survive, since what was signed is not what comes back."
      ],
      "faq": [
        {
          "q": "Does deleting pages from a PDF destroy the bookmarks?",
          "a": "In most tools yes, and here too until this release: a document holding six navigation entries came back holding none of them. Each entry belonging to a surviving sheet is now re-aimed at that sheet's new position, and the note that comes with the download says how many of the entries found made the journey."
        },
        {
          "q": "What happens to a form I have already filled in?",
          "a": "Anything typed into a sheet that stays is still there afterwards, and the shortened document is given back the dictionary a reader needs in order to show it. A box that only ever existed on a discarded sheet leaves with that sheet."
        },
        {
          "q": "I typed a page number that does not exist. What happens?",
          "a": "The job stops and says which number was wrong and how long the document actually is. It is emphatically not read as an instruction to change nothing, which would hand back an untouched file wearing a success message."
        },
        {
          "q": "Can I remove every page?",
          "a": "No, and the attempt is stopped in words. A document holding nothing has never been what anybody was after."
        },
        {
          "q": "Does anything leave my machine?",
          "a": "Nothing does. Opening, previewing, trimming and saving all run inside this tab from code shipped with the page."
        }
      ],
      "related": [
        "pdf-extract-pages",
        "pdf-split",
        "pdf-reorder",
        "pdf-merge"
      ]
    },
    {
      "slug": "pdf-extract-pages",
      "name": "Extract pages",
      "category": "pdf",
      "one_liner": "Write chosen pages into a new PDF, in the order you name them, with their bookmarks.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfop",
      "op": "extract",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Extract Pages from a PDF into a New File | Keeps Bookmarks",
      "search_description": "Name the pages to pull out as 1, 3-5, end or reverse and get a new PDF holding exactly those pages in that order, with their bookmarks, field values and document details intact.",
      "search_intro": "Lifting a selection out of a PDF creates something new, and the new thing deserves to arrive intact rather than as a stack of pictures. What usually comes back has the right imagery and nothing around it: a navigation panel with no entries, anything interactive rendered inert, and an identity belonging to whichever library performed the copy. This builds precisely the selection asked for, arranged precisely as asked, and brings its surroundings along. Arrangement is yours: the request reads left to right, so writing 5, 1-3 puts the fifth sheet ahead of the first three, and naming a sheet twice produces it twice. Navigation belonging to lifted sheets travels and is renumbered where those sheets landed, while navigation belonging to what stayed behind is discarded rather than misdirected. Typed-in values and the identity of the original, its name, author, subject and keywords, all make the trip.",
      "how_it_works": [
        "Open a source document. This tab measures its length and counts its navigation entries and interactive boxes before writing anything at all.",
        "Say what to lift. Clicking builds a rising sequence; writing unlocks the full vocabulary, spans such as 3-7, falling spans such as 7-3, unbounded spans such as 4-, plus end, odd, even, all and reverse.",
        "Watch the running tally and the summary beneath it, which say how long the new document will be and what becomes of every kind of content the original was carrying.",
        "Build it, then collect the new document alongside a JSON note listing the originating sheets in the sequence they were written and the navigation and interactive counts that made it across."
      ],
      "use_cases": [
        "Lift a single chapter out of a thick manual so it opens on its own, navigation panel and all.",
        "Take just the countersigned sheet from a returned agreement without losing what was typed into it.",
        "Assemble a reading sequence from a scan by writing sheets out of order, say 1, 5-7, 3."
      ],
      "limits": [
        "A single unprotected source, no larger than 50 MiB, producing no more than 200 sheets. Protected or corrupt sources halt the job by name. Its terminal counterpart works at any length.",
        "Asking for a sheet beyond the end halts the job, and a request resolving to nothing at all halts it too, rather than producing a document with nothing inside.",
        "Where a sheet is written more than once, its navigation entry binds to the earliest copy. Cross references, attached files, printed sheet labels and accessibility structure do not travel.",
        "Cryptographic signing does not survive, because a selection is a different document from the whole that was signed."
      ],
      "faq": [
        {
          "q": "Can I extract pages in a different order from the original?",
          "a": "Yes. Requests read left to right, so 5, 1-3 puts the fifth sheet first and the opening three behind it. Falling spans such as 7-3 work, and so does the word reverse."
        },
        {
          "q": "Do the bookmarks come with the extracted pages?",
          "a": "They do now. Until this release the lifted selection arrived with an empty navigation panel. Entries belonging to lifted sheets are re-aimed at wherever those sheets landed, and the accompanying note counts how many made it."
        },
        {
          "q": "Can I take the same page twice?",
          "a": "Yes. Name a sheet repeatedly and it appears repeatedly. Its navigation entry attaches to the earliest copy written."
        },
        {
          "q": "What is the difference between this and splitting a PDF?",
          "a": "Splitting carves one source into several outputs at once. This produces exactly one output holding exactly the selection named, arranged as named, which is what suits a single excerpt rather than a set of parts."
        },
        {
          "q": "Does anything leave my machine?",
          "a": "Nothing does. Opening, previewing, lifting and saving all happen inside this tab using code delivered with the page."
        }
      ],
      "related": [
        "pdf-delete-pages",
        "pdf-split",
        "pdf-merge",
        "pdf-reorder"
      ]
    },
    {
      "slug": "pdf-crop",
      "name": "Crop PDF",
      "category": "pdf",
      "one_liner": "Keep exact visible margins page by page, then download only a reopened, verified PDF and receipt.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfcrop",
      "op": "crop",
      "script": null,
      "tests": "rotation-safe CLI + verified browser",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Crop PDF Pages Locally | Precise Visible Margins",
      "search_description": "Crop PDF pages locally with visual or numeric margins that stay correct on rotated pages. Apply explicit page scopes, verify the output, and download a receipt.",
      "search_intro": "A crop box is easy to draw and surprisingly easy to apply to the wrong side of a rotated PDF page. This workbench treats top, right, bottom, and left as the sides you can actually see, maps that kept area back into each page's PDF coordinates, and shows the choice per physical page before it builds anything. Numeric margins provide the complete non-drag path. The result is opened again by two independent browser paths, and the PDF and its hash-bound receipt appear only when the planned page boxes, rotations, page count, and visible preview agree.",
      "how_it_works": [
        "Choose exactly one PDF up to 50 MiB and 60 pages. Page facts, feature warnings, thumbnails, and the active preview are read locally in this tab.",
        "Set visible top, right, bottom, and left margins with labelled numbers or the kept-area overlay. Review the current page, then atomically copy that crop to all, odd, even, or an explicit physical-page range.",
        "A dedicated builder creates the candidate and a separate verifier reopens it. Check the before-and-after box table and output preview, then download the current PDF and JSON receipt."
      ],
      "use_cases": [
        "Trim scanner borders or registration marks while keeping different choices on individual pages.",
        "Apply the same physical or proportional visible margins across pages with mixed sizes and rotations.",
        "Prepare a page range for reading or printing with a receipt that records the exact source, plan, boxes, and checks."
      ],
      "limits": [
        "Cropping changes page boxes and hides content; it does not remove the underlying PDF objects and is not secure redaction.",
        "The browser accepts one unencrypted PDF up to 50 MiB and 60 pages. Canvas axes are capped at 8,192 pixels, the active preview at 8 million pixels, all thumbnails at 12 million pixels, output at 100 MiB, and the receipt at 1 MiB.",
        "Signed documents and files with advanced or incompletely profiled features require an explicit plan-bound acknowledgement. The tool does not claim arbitrary PDF semantics or signatures are preserved.",
        "Once this page has loaded, the selected PDF bytes, crop plan, generated PDF, and receipt are not uploaded by this tool; normal same-origin route and bundled-asset requests may still occur."
      ],
      "faq": [
        {
          "q": "Does cropping permanently remove hidden PDF content?",
          "a": "No. A PDF CropBox changes what a normal viewer shows, but objects outside that box can remain in the file. Use the secure-redaction tool when information must be removed rather than hidden."
        },
        {
          "q": "What does top mean on a rotated PDF page?",
          "a": "Top means the top edge you see in the preview. The workbench maps visible sides through each page's own rotation and effective visible box before writing PDF coordinates."
        },
        {
          "q": "Can I crop a PDF without dragging?",
          "a": "Yes. The labelled top, right, bottom, and left fields, unit selector, page controls, scope controls, Apply, and Reset buttons provide the complete workflow."
        },
        {
          "q": "Can different PDF pages have different crops?",
          "a": "Yes. Every physical page keeps its own crop. You can edit one page directly or atomically copy its visible margins to all, odd, even, or an explicit page range."
        },
        {
          "q": "Is the PDF uploaded?",
          "a": "No file, plan, generated PDF, or receipt is uploaded by this tool. Processing happens in the tab after the normal page and bundled assets have loaded."
        }
      ],
      "related": [
        "pdf-rotate",
        "pdf-add-margins",
        "pdf-redact",
        "pdf-split",
        "pdf-reorder"
      ]
    },
    {
      "slug": "pdf-compress",
      "name": "Compress PDF",
      "category": "pdf",
      "one_liner": "Rasterize a PDF locally, compare a real page, and download only a smaller page-bound result.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfop",
      "op": "compress",
      "script": null,
      "tests": "69 CLI + 29 browser",
      "from": null,
      "to": null,
      "public": false,
      "search_title": "Rasterize and Compress PDF Locally | Compare Results",
      "search_description": "Build a JPEG-backed PDF locally. Review raster-image evidence, exact byte savings, visible feature loss, and a source-to-output page binding before download.",
      "search_intro": "PDF compression is not automatically an improvement. A page-flattening pass can make a small searchable document larger while removing its text, links, forms, tags, and signatures. This workbench reports extracted text, document features, and observed raster-image paint operators; it does not infer scan quality from missing text. It then builds an explicitly image-only candidate, binds each final page to its planned DCT JPEG bytes and full-page draw operators, renders every result page, and holds the download unless the file is smaller and the visible sample has been reviewed.",
      "how_it_works": [
        "Choose one PDF within the displayed byte and page limits; its text, pages, document features, and raster-image paint operators are inspected locally without guessing that zero text means a scan.",
        "Choose an honest image target, acknowledge that flattening removes PDF semantics, and build a bounded candidate that includes PDF.js-renderable visible annotations and form appearances with no synthetic-page fallback.",
        "Compare the same source and result page, review exact bytes and verification checks, then download the current PDF and its hash-bound receipt."
      ],
      "use_cases": [
        "Reduce a scanner-created packet before an email or upload limit.",
        "Try a lower image resolution while keeping a representative page visible for readability review.",
        "Prove that a candidate is smaller and structurally readable before handing it off."
      ],
      "limits": [
        "Browser output is an image-only derivative: selectable text, vector scaling, links, forms, bookmarks, tags, attachments, layers, signatures, encryption, and other PDF behavior are not preserved.",
        "The browser accepts one PDF up to 50 MiB and 60 pages, with an 8,192-pixel canvas-side cap, 16 million pixels per page, 80 million pixels per run, and a 100 MiB candidate cap.",
        "A representative-page comparison does not prove that every page is visually acceptable. A larger candidate is reported as no size win and cannot be downloaded as a compressed result."
      ],
      "faq": [
        {
          "q": "Is this suitable for a searchable or signed PDF?",
          "a": "Usually not. The browser deliberately creates image-only pages, flattens PDF.js-renderable visible annotation and form appearances into pixels, and invalidates or removes searchable text and document features. Use the local CLI for a verified Ghostscript rewrite, and keep the source as the authoritative file."
        },
        {
          "q": "What happens if the new PDF is larger?",
          "a": "The workbench shows the byte increase and verification evidence but does not enable the PDF download or call the run successful. Try another setting or keep the source."
        },
        {
          "q": "Is the PDF uploaded?",
          "a": "No. After the route loads, diagnosis, rendering, PDF creation, comparison, hashing, verification, and downloads happen inside this browser tab."
        }
      ],
      "related": [
        "pdf-size-analyzer",
        "pdf-image-dpi-check",
        "pdf-extract-text"
      ]
    },
    {
      "slug": "pdf-rotate",
      "name": "Rotate pages",
      "category": "pdf",
      "one_liner": "Rotate all pages or just the ones you list.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfop",
      "op": "rotate",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-page-numbers",
      "name": "Page numbers",
      "category": "pdf",
      "one_liner": "Stamp clean page numbers where you want them.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfop",
      "op": "pagenum",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "images-to-pdf",
      "name": "Images to PDF",
      "category": "pdf",
      "one_liner": "JPGs and PNGs into one PDF, one image per page.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfop",
      "op": "img2pdf",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-reorder",
      "name": "Reorder PDF Pages",
      "category": "pdf",
      "one_liner": "Move reviewed page thumbnails without silently dropping the rest of the document.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfop",
      "op": "reorder",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false,
      "search_title": "Reorder PDF Pages Privately | Visual Page Organizer",
      "search_description": "Reorder PDF pages locally with thumbnails and keyboard-ready move controls. Preserve every page by default, review exclusions, and export a SHA-256 manifest.",
      "search_intro": "Typing one page number should not silently discard every other page. This organizer starts with a complete document, keeps the immutable source number visible on every thumbnail, and provides native earlier, later, start, end, exclude, and restore controls. A strict text order can lead the sequence while unlisted pages remain appended unless exclusion is deliberately reviewed.",
      "how_it_works": [
        "Choose a PDF up to the tested 50 MiB and 60-page browser limits and review its locally rendered page thumbnails.",
        "Move pages with visible buttons or enter the strict page language; every unlisted page stays in the output unless it is explicitly excluded.",
        "Review the final sequence, any omissions, and page-copy caveats, then download the PDF and its hash-bound JSON manifest."
      ],
      "use_cases": [
        "Fix scanned pages that arrived out of order without uploading the document.",
        "Move a cover, appendix, signature page, or insert while keeping the rest intact.",
        "Reverse a document or create a reviewed subset with a durable source-to-result page map."
      ],
      "limits": [
        "Browser input is capped at 50 MiB and 60 pages, and output at 250 MiB; larger jobs should use the local CLI.",
        "Browser page copying can lose or alter bookmarks, forms, attachments, labels, signatures, tags, actions, and other document-level features, so the caveat always requires acknowledgement.",
        "Reordering modifies the PDF and invalidates cryptographic signatures; it does not sanitize links, annotations, or active content that may remain on copied pages."
      ],
      "faq": [
        {
          "q": "Will unlisted PDF pages be deleted?",
          "a": "No. In the browser, unlisted pages append in their original order. Excluding pages is a separate reviewed action, and the download stays disabled until that omission is acknowledged."
        },
        {
          "q": "Can I reorder pages without dragging?",
          "a": "Yes. Every page has native Move earlier, Move later, Move to start, Move to end, and Exclude controls designed for keyboard, click, and tap use."
        },
        {
          "q": "Is the PDF uploaded?",
          "a": "No. After the route loads, page rendering, reordering, hashing, verification, and downloads run locally with bundled browser code."
        }
      ],
      "related": [
        "pdf-split",
        "pdf-delete-pages",
        "pdf-rotate"
      ]
    },
    {
      "slug": "pdf-watermark",
      "name": "Watermark PDF",
      "category": "pdf",
      "one_liner": "A diagonal text watermark across every page, at the opacity you choose.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfop",
      "op": "watermark",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-bates",
      "name": "Bates numbering",
      "category": "pdf",
      "one_liner": "Legal Bates stamps: prefix, start number, zero-padded, one per page.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfop",
      "op": "bates",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-proof-stamp",
      "name": "Proof stamp",
      "category": "pdf",
      "one_liner": "DRAFT / APPROVED / CONFIDENTIAL and more, stamped big on every page.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfop",
      "op": "proofstamp",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-metadata-scrub",
      "name": "Scrub metadata",
      "category": "pdf",
      "one_liner": "Strip author, title, producer and other identifying metadata.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfop",
      "op": "scrub",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-nup",
      "name": "N-up imposition",
      "category": "pdf",
      "one_liner": "Print 2 or 4 pages on one sheet.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfop",
      "op": "nup",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-booklet",
      "name": "Booklet imposition",
      "category": "pdf",
      "one_liner": "Reorder pages into a foldable saddle-stitch booklet.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfop",
      "op": "booklet",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-add-margins",
      "name": "Add margins",
      "category": "pdf",
      "one_liner": "Add clean white margins around every page for binding or notes.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfop",
      "op": "margins",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-flatten-forms",
      "name": "Flatten form fields",
      "category": "pdf",
      "one_liner": "Bake filled form fields into the page so they cannot be changed.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfop",
      "op": "flattenforms",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-attach-file",
      "name": "Attach a file",
      "category": "pdf",
      "one_liner": "Embed any file inside the PDF so it travels with the document.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfop",
      "op": "attach",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-extract-attachments",
      "name": "Extract attachments",
      "category": "pdf",
      "one_liner": "Pull embedded file attachments back out of a PDF.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfop",
      "op": "getattach",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-exhibit-label",
      "name": "Exhibit labels",
      "category": "pdf",
      "one_liner": "Stamp legal exhibit labels in a box on the page.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfop",
      "op": "exhibit",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-dupe-page-remove",
      "name": "Remove duplicate pages",
      "category": "pdf",
      "one_liner": "Drop repeated pages and keep the first of each.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfop",
      "op": "dedupe",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-blank-pages",
      "name": "Remove blank pages from PDF",
      "category": "pdf",
      "one_liner": "Measure visible ink, review every page, and remove empty or near-blank sheets.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfpreflight",
      "op": "blank-pages",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false,
      "search_title": "Remove Blank Pages from PDF Automatically | Private",
      "search_description": "Find and remove blank or near-blank PDF pages in your browser. Review measured ink coverage and every page preview before downloading. No uploads.",
      "search_intro": "Automatic blank-page tools often miss the white back of a duplex scan because that page still contains a full-page image. This scan renders each page locally, measures how much visible ink remains, and selects likely blanks without deleting anything until the page previews are reviewed.",
      "how_it_works": [
        "Choose strict, scanner-dust, or loose sensitivity and scan the PDF locally.",
        "Review every page preview, its measured ink coverage, and the suggested decision.",
        "Click any mistaken suggestion to keep or remove it, then download the cleaned PDF and optional JSON report."
      ],
      "use_cases": [
        "Remove white backs produced by duplex document scanners.",
        "Clean trailing empty sheets and blank separators before printing or sharing.",
        "Audit a large PDF quickly without trusting an automatic delete decision."
      ],
      "limits": [
        "A page with a tiny intentional mark can resemble scanner dust; every suggestion should be reviewed.",
        "Higher sensitivity may select faint handwriting, pale watermarks, or lightly printed pages.",
        "Removing pages can change printed page references and internal links in the source document."
      ],
      "faq": [
        {
          "q": "Does it remove scanned blank pages?",
          "a": "Yes. It measures a rendered preview, so a white full-page scan can be detected even though the PDF technically contains an image."
        },
        {
          "q": "Can it delete a nonblank page by mistake?",
          "a": "The scan only preselects candidates. Every page remains visible and can be switched between keep and remove before the new PDF is built."
        },
        {
          "q": "Is the PDF uploaded?",
          "a": "No. Rendering, measurement, page removal, and download all happen in this browser tab."
        }
      ],
      "related": [
        "pdf-delete-pages",
        "pdf-dupe-page-remove",
        "pdf-split"
      ]
    },
    {
      "slug": "pdf-image-dpi-check",
      "name": "Check PDF image DPI",
      "category": "pdf",
      "one_liner": "Measure every raster image at its placed print size and flag low effective resolution.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfpreflight",
      "op": "image-dpi",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false,
      "search_title": "Check PDF Image DPI Online | Private Print Preflight",
      "search_description": "Check effective image DPI or PPI inside a PDF before printing. Measure every raster placement at final size and export CSV or JSON. No uploads.",
      "search_intro": "A 300-PPI image can become a 150-PPI image when it is enlarged to twice its intended size. This preflight reads each PDF image's native pixel dimensions and placement transform, then reports the effective resolution on both axes at the size it will actually print.",
      "how_it_works": [
        "Set the minimum resolution for screen, everyday print, or close-view print work.",
        "Inspect every raster placement by page, native pixels, placed size, axis resolution, and pass or low status.",
        "Download the complete preflight as CSV for a print job or JSON for another workflow."
      ],
      "use_cases": [
        "Catch blurry photographs before a brochure, worksheet, or book goes to print.",
        "Locate the page where an image was enlarged beyond its useful resolution.",
        "Add a deterministic low-resolution gate to a print-production handoff."
      ],
      "limits": [
        "Vector artwork and text are resolution-independent and are not listed as raster images.",
        "Resolution is one print-quality factor; bleed, color space, fonts, transparency, and compression need separate checks.",
        "The report measures source pixels at placement size and cannot restore missing detail."
      ],
      "faq": [
        {
          "q": "Is this DPI or PPI?",
          "a": "For a raster image, pixels per inch is the precise term. The phrase image DPI is commonly used in PDF and print searches, so the interface shows effective PPI while supporting that familiar name."
        },
        {
          "q": "What threshold should be used?",
          "a": "300 PPI is a common target for close-view quality print, 150 PPI can suit everyday print, and lower values may be acceptable for screen use or large-format work viewed from farther away."
        },
        {
          "q": "Does the PDF leave the browser?",
          "a": "No. The PDF operator list and image placement transforms are inspected locally; the site has no upload endpoint."
        }
      ],
      "related": [
        "pdf-size-analyzer",
        "pdf-compress",
        "pdf-print-safe"
      ]
    },
    {
      "slug": "pdf-size-analyzer",
      "name": "Analyze PDF file size",
      "category": "pdf",
      "one_liner": "Find image-heavy pages, oversized source pixels, and the most likely reason a PDF is large.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfassets",
      "op": "size-analyzer",
      "script": null,
      "tests": "7/7",
      "from": null,
      "to": null,
      "public": false,
      "search_title": "Analyze PDF File Size | Find What Makes a PDF Large",
      "search_description": "Diagnose a large PDF privately in your browser. Measure decoded images, placements, source pixels, and effective PPI, then export a JSON report.",
      "search_intro": "A PDF compressor can make a file smaller without explaining what it changed. This analyzer starts with diagnosis: it counts unique decoded images, follows their page placements, measures source pixels and effective resolution, and points to likely scan or over-resolution problems. The companion CLI goes deeper by attributing exact stored bytes to image, font, page-content, form, and overhead categories.",
      "how_it_works": [
        "Choose the large PDF; PDF.js inspects its pages and raster image operators locally.",
        "Review unique images, repeat placements, decoded pixel memory, effective PPI, and focused suggestions.",
        "Download a JSON diagnosis, or run the CLI when an exact raw-stream byte budget is required."
      ],
      "use_cases": [
        "Find full-page scans or oversized photographs before choosing compression settings.",
        "Explain why a seemingly simple report is much larger than expected.",
        "Separate image problems from likely font, vector, attachment, revision, or structural overhead."
      ],
      "limits": [
        "Browser analysis measures decoded images and placements, not the compressed byte length of each stored PDF object.",
        "PDF.js may not expose every unusual image stream or malformed object.",
        "The analyzer diagnoses likely causes; it does not alter or compress the source file."
      ],
      "faq": [
        {
          "q": "Why is my PDF file so large?",
          "a": "Common causes include full-page scans, photographs stored at far more pixels than their placed size needs, embedded fonts, attachments, complex vector content, and incremental saved revisions. This browser report measures the image side; the CLI attributes exact stored bytes across all major stream categories."
        },
        {
          "q": "Does decoded pixel memory equal PDF file size?",
          "a": "No. Decoded memory estimates the working pixels required to display images. Stored streams may be much smaller because JPEG, PNG-style filters, and other PDF compression are applied."
        },
        {
          "q": "Is the PDF uploaded?",
          "a": "No. Inspection runs in this tab and the site has no file-upload endpoint."
        }
      ],
      "related": [
        "pdf-compress",
        "pdf-image-dpi-check",
        "pdf-extract-images"
      ]
    },
    {
      "slug": "pdf-extract-images",
      "name": "Extract images from PDF",
      "category": "pdf",
      "one_liner": "Recover each unique embedded image as a previewable PNG without rasterizing whole pages.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfassets",
      "op": "extract-images",
      "script": null,
      "tests": "8/8",
      "from": null,
      "to": null,
      "public": false,
      "search_title": "Extract Images from PDF Privately | Download as PNG",
      "search_description": "Extract embedded images from a PDF in your browser, preview every unique asset, and download individual PNGs or one ZIP. No uploads.",
      "search_intro": "Converting PDF pages to pictures is not the same as recovering the pictures inside them. This tool reads the PDF image operators, deduplicates repeated assets, shows where each one appears, and exports the decoded pixels as PNG without surrounding page text or margins. The CLI can preserve the original embedded image stream instead.",
      "how_it_works": [
        "Choose a PDF and set the minimum shorter-side dimension to skip tiny icons or masks.",
        "Review every unique decoded image with pixel dimensions, page appearances, and placement count.",
        "Download one PNG, all matching images as a ZIP, and a JSON manifest."
      ],
      "use_cases": [
        "Recover photographs from a brochure, deck, report, or archived PDF.",
        "Locate and reuse an image repeated across several pages without saving duplicates.",
        "Audit the native pixel dimensions of source images separately from the rendered pages."
      ],
      "limits": [
        "Browser downloads are decoded PNGs, so original JPEG compression and embedded file bytes are not preserved.",
        "Masks, color profiles, or unusual PDF image encodings can render differently from their underlying stream.",
        "Vector drawings and page text are not embedded raster images and are not extracted."
      ],
      "faq": [
        {
          "q": "Does this extract the original image quality?",
          "a": "It preserves the decoded pixel dimensions, but the browser re-encodes those pixels as PNG. Use the CLI when the exact original embedded JPEG or PNG stream is required."
        },
        {
          "q": "Why is an image listed only once when it appears on many pages?",
          "a": "PDFs commonly reuse one image object. The tool deduplicates that object and reports every page and placement count."
        },
        {
          "q": "Does it turn each PDF page into an image?",
          "a": "No. It extracts raster assets placed inside pages. Use PDF to images when one complete image per page is wanted."
        }
      ],
      "related": [
        "pdf-to-images",
        "pdf-size-analyzer",
        "pdf-image-dpi-check"
      ]
    },
    {
      "slug": "canva-extract-images",
      "name": "Extract images from Canva",
      "category": "image",
      "one_liner": "Get every image out of a Canva design at full resolution, with where each one sat.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pptxassets",
      "op": "extract-assets",
      "script": null,
      "tests": "19/19",
      "from": null,
      "to": null,
      "public": true,
      "search_title": "Extract Images From Canva Free and Without Uploading",
      "search_description": "Pull every photo, icon and graphic out of a Canva design at full resolution, plus a record of where each one sat on the page. Export as PowerPoint, drop it here. Nothing is uploaded.",
      "search_intro": "Canva has no way to download the images inside a design. Right-clicking gives you a screenshot at screen resolution, and downloading the design as PNG flattens everything into one picture. Export the design as PowerPoint instead, drop the file here, and every image comes back at the resolution it was placed at, along with a record of where it sat on the page: position, size, rotation, crop and stacking order. It reads any PowerPoint or Google Slides export the same way, and every page is redrawn from the extracted pieces so you can see the record is right before you trust it.",
      "how_it_works": [
        "In Canva, open Share, then Download, and choose PowerPoint (PPTX) as the file type.",
        "Drop that file here. Every image appears as a picture with its pixel size and how many times the design uses it.",
        "Save the ones you want as a zip, and the layout record as layout.json."
      ],
      "use_cases": [
        "Reuse the photos and graphics from a design without re-buying or re-cropping them.",
        "Rebuild a Canva layout somewhere else from a record of the real positions.",
        "See which images a design actually carries, and at what resolution, before handing it to a printer."
      ],
      "limits": [
        "Text, charts, embedded video and Canva's own effects are not part of the record.",
        "Images the browser cannot decode, including SVG parts, are named rather than dropped.",
        "The browser re-encodes the pixels; the local tool can also keep the untouched original file.",
        "A placement that cannot be reduced to position, size and angle is marked and says why."
      ],
      "faq": [
        {
          "q": "Why not just right-click and save the image in Canva?",
          "a": "Canva does not offer it on a design you did not upload the image to, and a screenshot is limited to what fits on your screen. The PowerPoint export carries the original file at the resolution it was placed at."
        },
        {
          "q": "Why does every other extractor find nothing in my Canva file?",
          "a": "Canva places each image as the fill of a shape rather than as a picture, and the usual libraries only look for pictures. A test file here holds 174 images and zero pictures by that definition. This reads both, plus page backgrounds."
        },
        {
          "q": "Does it work on PowerPoint and Google Slides files too?",
          "a": "Yes. Any .pptx is read the same way, whichever tool made it."
        },
        {
          "q": "What is in layout.json?",
          "a": "One entry per image and one per appearance of it on a page, with four corner points in the units the file uses, plus rotation, crop fractions, opacity, stacking order and the page size."
        },
        {
          "q": "Is my design uploaded?",
          "a": "No. The file is opened, read and re-encoded in this tab, and the site has no upload endpoint."
        }
      ],
      "related": [
        "pdf-extract-images",
        "image-optimizer",
        "pdf-to-slides"
      ]
    },
    {
      "slug": "pdf-redact",
      "name": "Securely redact a PDF",
      "category": "pdf",
      "one_liner": "Find sensitive text, review every region, and download an image-only PDF with no extractable source text.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfredact",
      "op": "secure-redact",
      "script": null,
      "tests": "13/13",
      "from": null,
      "to": null,
      "public": false,
      "search_title": "Secure PDF Redaction Online | Remove Text, Not Cover It",
      "search_description": "Redact PDF text securely in your browser. Find phrases or sensitive-data patterns, add manual boxes, rasterize the result, and verify zero extractable text.",
      "search_intro": "A black rectangle is not a secure redaction when the words remain selectable underneath it. This workflow finds exact phrases and common sensitive-data patterns, makes every proposed region reviewable, supports manual boxes for scans, then rasterizes each page with the chosen areas burned to black. The downloaded PDF is reopened and must contain zero extractable text before it is released.",
      "how_it_works": [
        "Enter exact phrases or choose email, SSN, phone, Luhn-valid card, or IPv4 patterns.",
        "Review the highlighted pages, ignore false positives, and drag manual boxes over scans or split text runs.",
        "Download an image-only redacted PDF plus a JSON report after the browser verifies no text remains extractable."
      ],
      "use_cases": [
        "Remove account details, contact information, identifiers, or client names before sharing.",
        "Redact image-only scans by drawing manual regions directly on each page preview.",
        "Avoid the common failure where a visual black bar leaves the underlying text copyable or searchable."
      ],
      "limits": [
        "Secure browser output intentionally discards all source text, links, forms, attachments, metadata, and accessibility structure.",
        "A phrase split across separate PDF text runs may not match automatically and needs a manual box.",
        "Automated patterns can have false positives or miss unusual formats; every page must be reviewed."
      ],
      "faq": [
        {
          "q": "Is drawing a black box over PDF text secure?",
          "a": "Not by itself. An overlay can leave the original text or image data in the file. This browser tool creates new rasterized pages with the selected regions burned into their pixels, then verifies the result has no extractable text."
        },
        {
          "q": "Will the redacted PDF still be searchable?",
          "a": "No. Removing all source text is the browser workflow's security boundary. The local CLI performs object-level redaction and can preserve unredacted searchable text."
        },
        {
          "q": "Does the file leave my device?",
          "a": "No. Matching, page rendering, redaction, verification, and download happen locally in this tab."
        }
      ],
      "related": [
        "pdf-metadata-scrub",
        "pdf-flatten-forms",
        "pdf-extract-text"
      ]
    },
    {
      "slug": "pdf-split-by-size",
      "name": "Split by size",
      "category": "pdf",
      "one_liner": "Break a large PDF into parts under a size you set.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfop",
      "op": "splitsize",
      "script": null,
      "tests": "",
      "from": null,
      "to": null,
      "public": false
    },
    {
      "slug": "pdf-ocr",
      "name": "OCR Evidence Workbench",
      "category": "pdf",
      "one_liner": "OCR images and scanned PDF pages locally, then review uncertain words against the raw engine evidence.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "ocrworkbench",
      "op": "ocr",
      "script": null,
      "tests": "153 CLI + 20 browser",
      "from": null,
      "to": null,
      "public": false,
      "search_title": "OCR Images and Scanned PDFs Privately | Evidence Workbench",
      "search_description": "OCR images and scanned PDF pages locally in your browser. Review uncertain words, compare evidence, preserve punctuation, and export editable text or JSON.",
      "search_intro": "A successful-looking text box is not proof of a faithful transcription. This workbench runs a pinned Tesseract LSTM engine locally, keeps every observed pass, chooses one whole observed pass per page instead of inventing unseen text, and lets you correct the editable result without rewriting the raw evidence.",
      "how_it_works": [
        "Choose one PNG, JPEG, WebP, BMP, or PDF and select English, Spanish, or both languages.",
        "Run quick or redundant OCR; PDF pages are rendered locally, every scheduled pass retains its status, configuration, and warnings, and every valid completed pass retains text, TSV geometry, and hOCR.",
        "Review low-confidence words beside the source, edit the complete result, then export current text, canonical JSON, or a raw evidence ZIP."
      ],
      "use_cases": [
        "Recover English or Spanish text from screenshots, scans, photographed pages, and image-only PDFs.",
        "Inspect punctuation, accents, code-like spacing, and ambiguous words instead of hiding disagreement behind one output.",
        "Keep an auditable engine record separate from later human corrections."
      ],
      "limits": [
        "Browser input is capped at 25 MiB, 12 PDF pages, 12 million pixels per page, 48 million rendered pixels, 36 OCR passes, and 64 MiB of uncompressed evidence; balanced mode therefore accepts at most nine pages.",
        "Handwriting, equations, arbitrary scripts, exact reading order, tables, typography, bold or italic reconstruction, and universal font fidelity are not promised; review the source and uncertain tokens.",
        "The browser surface does not create searchable PDFs or ALTO and is intentionally narrower than the local CLI evidence pipeline."
      ],
      "faq": [
        {
          "q": "Does the image or recognized text leave this device?",
          "a": "No. The pinned worker, OCR core, English and Spanish models, and PDF support files are served as static same-origin assets; file bytes and recognized text stay in this tab and no OCR cache is persisted."
        },
        {
          "q": "Is the confidence score a probability that a word is correct?",
          "a": "No. It is raw engine evidence used to prioritize review, not a calibrated probability or a guarantee."
        },
        {
          "q": "Why preserve several passes?",
          "a": "Different preprocessing and page-segmentation assumptions expose different mistakes. The workbench retains those observations, while the canonical page remains one traceable engine pass."
        }
      ],
      "related": [
        "pdf-extract-text",
        "pdf-to-images",
        "pdf-image-dpi-check",
        "image-diff"
      ]
    },
    {
      "slug": "pdf-extract-text",
      "name": "Extract structured PDF text",
      "category": "pdf",
      "one_liner": "Rebuild selectable text in reading order, correct it against the source page, and export Markdown, text, or JSON locally.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfstructure",
      "op": "extract-text",
      "script": null,
      "tests": "4 CLI + 12 structure-editor",
      "from": null,
      "to": null,
      "public": false,
      "search_title": "Extract and Edit PDF Text Privately | Markdown or JSON",
      "search_description": "Extract and edit a PDF text layer locally. Review source-linked blocks, fix reading order and headings, then export Markdown, text, or structured JSON.",
      "search_intro": "Use this when copy and paste scrambles a report, paper, manual, or two-column PDF. It reads positioned text already embedded in the file, reconstructs reading order, links every extracted block to its source-page box, and keeps the structured result editable before export.",
      "how_it_works": [
        "Choose a born-digital PDF and the pages you need.",
        "Review each editable block beside its highlighted source-page location; correct text, heading levels, and order with undo and redo.",
        "Find and replace, clean whitespace, then copy or download Markdown, plain text, or JSON generated from the current edits."
      ],
      "use_cases": [
        "Move a report into notes or a knowledge base without uploading it.",
        "Recover paragraph order from one- and two-column documents.",
        "Create page-referenced JSON for search, accessibility review, or another local tool."
      ],
      "limits": [
        "This extracts an existing text layer; it does not OCR image-only scans.",
        "Complex magazines, tables, equations, and decorative layouts may still need manual cleanup.",
        "The browser workbench accepts up to 50 MiB, 150 pages, 250,000 text runs, and 5 MiB per exported text artifact; larger work belongs in the CLI."
      ],
      "faq": [
        {
          "q": "Does this upload my PDF?",
          "a": "No. PDF.js reads the file in this browser tab; the site has no upload or conversion endpoint."
        },
        {
          "q": "Why is the result empty?",
          "a": "The chosen pages are probably scanned images without selectable text. Run OCR first, then extract the new text layer."
        },
        {
          "q": "What makes this different from copying all text?",
          "a": "It uses each text run's position and size to rebuild columns, paragraphs, headings, page references, and structured JSON."
        }
      ],
      "related": [
        "pdf-toc",
        "pdf-to-markdown",
        "pdf-to-webpage"
      ]
    },
    {
      "slug": "pdf-toc",
      "name": "Extract PDF bookmarks and table of contents",
      "category": "pdf",
      "one_liner": "Extract or infer PDF bookmarks, repair the hierarchy and destinations, then export a reviewed outline locally.",
      "replaces": "",
      "primitive": "",
      "status": "done",
      "runner": "pdfstructure",
      "op": "toc",
      "script": null,
      "tests": "6 CLI + 12 structure-editor",
      "from": null,
      "to": null,
      "public": false,
      "search_title": "Extract and Edit PDF Bookmarks Privately | Outline Editor",
      "search_description": "Read and edit PDF bookmarks locally. Repair titles, hierarchy, order, and page destinations, then export Markdown, text, CSV, or JSON.",
      "search_intro": "A PDF's sidebar bookmarks are structured data, but most readers make them awkward to copy or correct. This workbench extracts the complete hierarchy, clearly separates embedded bookmarks from inferred heading suggestions, previews every page destination, and keeps the outline editable before export.",
      "how_it_works": [
        "Drop in a PDF and choose embedded bookmarks, automatic fallback, or heading inference.",
        "Preview destination pages and repair titles, levels, ordering, indentation, duplicates, and unresolved page numbers with undo and redo.",
        "Copy or download Markdown, text, CSV, or JSON generated from the current reviewed outline."
      ],
      "use_cases": [
        "Reuse a book or manual's bookmark tree in documentation or a website.",
        "Audit whether a long PDF has usable navigation and page destinations.",
        "Draft a contents list for a born-digital report that has no embedded outline."
      ],
      "limits": [
        "Inferred entries use typography, not semantic understanding, and must be reviewed.",
        "Scanned PDFs need OCR before headings can be inferred.",
        "This version edits exported outline data; it does not write bookmarks or arbitrary PDF actions back into the source file."
      ],
      "faq": [
        {
          "q": "Are PDF bookmarks the same as a table of contents?",
          "a": "Bookmarks are the hierarchical links shown in a PDF reader's navigation pane. They often mirror a printed contents page, but the two can differ."
        },
        {
          "q": "What happens when the PDF has no bookmarks?",
          "a": "Automatic mode can propose an outline from unusually large text. Every inferred result is labeled and includes a review warning."
        },
        {
          "q": "Does this upload the document?",
          "a": "No. Bookmark and text-layer analysis happens locally in this browser tab."
        }
      ],
      "related": [
        "pdf-extract-text",
        "pdf-to-markdown",
        "pdf-split"
      ]
    }
  ]
}
