feat: proportional image display with inline scaling and precache fixes Replace the fixed IMAGE_DISPLAY_H (200px) cap with a proportional system that scales inline images to 40% of the text area while preserving aspect ratio, and decodes fullscreen images at the full text area budget. Image dimension pre-scan: - Add prescan_image_heights() to scan page buffers for IMG_REF markers and resolve each image's display height before wrapping - peek_cached_image_size() reads the 4-byte cache header (no pixel data) for already-decoded dimensions - peek_source_dimensions() peeks PNG IHDR (29 bytes) or JPEG SOF (up to 32KB header) directly from stored ZIP entries, computes the decoder's integer downscale output height; deflate-compressed entries fall back to DEFAULT_IMG_H (350px) Proportional line reservation: - wrap_proportional() takes an img_heights slice and reserves the exact number of lines each image needs at its actual height - Inline heights capped to inline_img_max_h() (40% of text area, ~308px on default theme); fullscreen bypass uses full budget Decode-time scaling: - decode_page_images() uses the inline budget for non-fullscreen pages, full text_area_h for fullscreen - Oversized cache hits (precached at full budget) are skipped and re-decoded at the inline budget so images are properly scaled rather than clipped Vertical centering: - Inline images are centered within their reserved line block instead of top-aligned, eliminating excess bottom margin Precache reliability: - Add work_queue::can_submit() to check channel capacity before expensive ZIP extraction; prevents wasted deflate + alloc when the worker channel is already full - Change queue-full race from permanent skip to retry on next poll - Add OOM retry for streaming decode: release ch_cache (~96KB) and retry once, matching the existing pattern in decode_page_images